Darcy's Blog

不如烂笔头


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

Impala内存配置

发表于 2019-10-30 | 分类于 优化 |

Impala在执行的过程中经常遇到OOM的问题,导致我们的有些ETL失败,经常要重跑任务。后面研究发现是我们的配置出现问题了,修改完配置后OOM就再也没有出现过了。

问题

我们的Impala在使用的时候经常会遇到OOM的问题,之前是把Impala和yarn等工具都部署在同一集群中,而且所有工具的内存使用量加起来是大于集群所拥有的实际物理内存的量。所以每次Impala OOM发生的时候,我们都以为是我们的内存不够造成的。但是后面我们把Impala部署到一个单独集群后(有充足的内存),偶尔还是会出OOM的情况。

原因

我们之前的Impala内存配置是这样的:

mem_limit: 70G
Impala Daemon JVM Heap: 60G

我们本来以为这样的配置就相当于,Impala总共使用的内存量是70G,但是实际上Impala可以使用的内存量是130G,如果我们的机器的内存小于130G(我们的内存实际上大概是80G)就会发生OOM。

解决方案

Impala Daemon进程其实是由两个不同的进程组成的。一个进程是用C++写的,它主要用来执行查询语句;另外一个进程是用Java写的,它主要是用来编译执行语句和存储metadata信息。Java的进程被嵌入到C++的进程中,因此他们两个进程共享一个进程号。上述的两个内存参数:mem_limit是用来控制C++进程的内存使用量,而Impala Daemon JVM Heap是用来控制Java进程的内存使用量的。

我们都知道Impala的内存使用量主要都是花费在执行查询的阶段,而存储的metadata的内存量不用太多,所以我们把配置改成如下的方案,就再也没有发生过OOM了。

mem_limit: 70G
Impala Daemon JVM Heap: 10G

参考资料

  1. HOW TO CONTROL IMPALA DAEMON’S MEMORY LIMIT

airflow使用总结

发表于 2019-07-20 | 分类于 任务调度 |

etl定时任务通常会使用一些任务调度的工具,我们之前使用的是azkaban,现在已经切换到airflow了。总的来说airflow的功能强大不少,但是坑也不少,以下是对airflow的一些总结。

对比azkaban,airflow有哪些优势?

  • airflow使用python编写,源代码比较容易理解,而且方便进行二次开发。比如可以很方便的定制自己的Operator。
  • dag(也就是任务流)使用python代码实现,可以根据复杂的条件生成相应的dag,非常的灵活。
  • 提供连接管理的功能,可以事先在airflow的web页面上配置各种连接,而使用者只要指定连接的名字就可以了。该功能方便让配置和实现进行解耦,以后修改连接也只需要在web页面上操作,不需要修改代码。
  • 提供资源池的功能,能够让不同的任务使用同一资源池,从而通过资源池来控制任务的并发度,方便进行资源限制。
  • 任务重跑也非常方便,在web页面上只要clear相应的任务,airflow的调度器就会自动重跑该任务。

当然,airflow的优势不仅仅如此,只有自己真正使用了,才能体会airflow的强大之处。不过功能越强大,也意味着使用起来也越复杂,下面是我总结的一些airflow的使用的注意事项。

airflow使用注意事项

  • airflow默认使用sqlite数据库,使用sqlite的时候airflow的调度器使用的是SequentialExecutor,该Executor只能顺序运行任务,同一时间只能有一个任务执行。
  • 如果想让一个dag同一时间只能运行一个dag实例的话,需要在配置文件里面把max_active_runs_per_dag配置成1.
  • airflow默认会载入example的那些dag,如果不想载入的话,设置load_examples = False.
  • airflow的api接口默认没有要求权限验证,需要配置权限验证的话,需要在[api]配置块下配置auth_backend,比如可以配置成auth_backend = airflow.contrib.auth.backends.password_auth。
  • airflow的web页面如果想启用用户权限验证的话,可以配置rbac=True。注意:这边的权限和api的权限是分开配置的。
  • airflow的dag需要配置一个start_date,dag执行的时候默认会从start_date开始一次次执行,直到当前时间。如果不想要这个功能的话,可以配置catchup_by_default = False,把该功能关掉。

使用k8s部署fluentd日志收集系统

发表于 2019-04-07 | 分类于 日志收集 |

在前一篇文章Fluentd性能优化实践中,我们的fluentd日志收集系统已经改成了多进程的方式。但是由于日志的不断增加,普通的单机部署方式也不能满足我们的需求,所以我们就打算把fluentd部署到k8s上,以满足不断增加的日志需求。

为什么需要部署到k8s上?

如下图所示,在多进程架构中,我们所有的out进程都是部署在同一台机器上,当日志量不断增多的时候,out进程数量就要不断增加,当增加到一定程度的时候,就会把out进程所在的这台机器的所有资源都用完,这时候我们就需要升级该机器,因此该架构的问题就暴露出来了。 Fluentd多进程架构 该架构的主要问题是:

  • 升级的过程需要手动操作,升级过程比较繁琐
  • 单台机器的性能不可能无限的增长
  • 资源不能动态的调整,如果机器升级只是为了应付某个波峰的话,会造成性能浪费

怎么部署到k8s上?

由于我们每个fluentd进程的buffer都会使用file的方式,所以我们的fluentd进程是有状态的,因此我们在k8s上部署的时候采用statefulset的部署方式。新的日志收集架构图如下所示: Fluentd部署到k8s后的架构 使用该架构后以上的单机问题就没有了,当日志量不断增加的时候,在k8s集群上面我们可以一直增加out进程的数量而不会受到单机性能的限制,甚至我们还可以让fluentd的out进程的数量根据实际的资源使用情况来自动的伸缩。

总结

这篇文章的内容比较简单,但是实实在在为我们解决了一个大问题,让我们在每次日志内容增多的时候都能从容应对。同时应用容器化是一件非常值得做的事情,build once run everywhere,可以避免很多不必要的问题,同时在替换一些基础依赖的时候也非常方便。

spark执行map-join优化

发表于 2019-03-09 | 分类于 优化 |

在使用map reduce处理数据的时候,join操作有两种选择:一种选择是在map端执行join操作,即所谓的Map-side Join(Broadcast join);另一种选择是在reduce端执行join操作,即所谓的Reduce-side Join(shuffle join)。在map端执行join操作,适合在有一个表比较小的情况下,能把整个表放到内存,发送到各个节点进行join操作。下面的文章主要介绍在spark使用Map-side Join的时候需要注意的问题。

spark使用map-join的时候可能会遇到什么问题?

我在执行以下sql语句的时候遇到了执行内存不够的问题,原先我还以为是内存不够,然后加大了内存,但是还是执行失败。

1
2
// a是一个几亿行的大表,b是一个只有几十行的小表。a和b都是由hive创建的表
select * from a where id not in (select id from b)

后面我在spark ui中看到了该sql的执行计划,该sql语句执行了Map-side Join操作,但是spark把a表当成了小表,准备把a表broadcast到其他的节点,然后就是一直卡在这步broadcast操作上。 造成上述问题的原因就是spark认为a表是一个小表,但是在spark ui上明显可以看到a表读了很多的行。但是为什么spark还会认为a表是一个小表呢?原因是spark判断一个hive表的大小会用hive的metastore数据来判断,因为我们的a表没有执行过ANALYZE TABLE,自然a表的metastore里面的数据就不准确了。

解决方法?

既然知道了问题,要解决就很简单了。有如下几个解决方法:

设置spark.sql.statistics.fallBackToHdfs=True

该参数能让spark直接读取hdfs的文件大小来判断一个表达大小,从而代替从metastore里面的获取的关于表的信息。这样spark自然能正确的判断出表的大小,从而使用b表来进行broadcast。

使用hint

在使用sql语句执行的时候在sql语句里面加上mapjoin的注释,也能够达到相应的效果,比如把上述的sql语句改成:

1
select /*+ BROADCAST (b) */ * from a where id not in (select id from b)

这样spark也会使用b表来进行broadcast。

使用spark代码的方式

使用broadcast函数就能达到此效果:

1
2
from pyspark.sql.functions import broadcast
broadcast(spark.table("b")).join(spark.table("a"), "id").show()

拓展知识

什么时候spark才会使用Map-side Join?

只有当要进行join的表的大小小于spark.sql.autoBroadcastJoinThreshold(默认是10M)的时候,才会进行mapjoin。

总结

上述的优化同样也适用于其他的计算引擎,比如Impala通过hint和执行表的位置调整也能够优化join操作,通过explain也可以查看sql的执行计划,然后再进行优化。

Hadoop系统Parquet文件的Timestamp类型的时区问题

发表于 2019-02-14 | 分类于 问题研究 |

在数据仓库离线处理中,为了能够兼容不同的数据处理引擎,我们通常会把数据保存成Parquet格式的文件。但是在不同的数据处理引擎中会出现时间读取不一致的问题,比如我用impala写入的时间,在用hive和spark读取的时候会差几个小时,这是为什么呢?

Hive中的Timestamp

Hive在0.8的版本后开始支持Timestamp的格式。Hive在储存时间戳的时候会先把时间转成UTC的时间,然后再把转换后的时间存储到Parquet文件中。在读取Parquet文件的时候Hive会把时间从UTC时间再转化回成本地的时间。这样的话,如果存和读取都是用Hive的话,时间不会有任何的问题。上述说的是用Parquet文件来存取时间格式流程,如果是存成普通的文本文件的话,存取都不会进行任何时间的转换。

Impala中的Timestamp

Impala在存取Parquet格式的时间的时候和Hive不一样,Impala在存储时间的时候不转换成UTC的时间,而是直接保存当前时区的时间。所以Impala在读的时候也不进行任何的转换,是直接读取Parquet文件中的时间。这样的话,如果存和读取都是用Impala的话,时间也不会有任何的问题。但是如果用Hive存,但是用Impala读的话,就会出现时间不一致的问题,反之亦然。 由于让Impala和Hive之间的时间格式互相兼容是一件比较重要的事情,从而出现了让两个系统的时间格式互相兼容的解决方案。

让Hive和Impala互相兼容

Impala正确读取Hive存储的Parquet时间

在Impala的配置里面设置

convert_legacy_hive_parquet_utc_timestamps=true (default false)

该参数会让Impala在读取Parquet文件的时候,会先检查Parquet文件的meta信息,查看该Parquet文件是否由Hive创建,如果是由Hive创建的话,在读取的时候会进行时间格式的转换,这样读取出来的时间就能和Hive存进去的时间一致了。

Hive正确读取Impala存储的Parquet时间

同样在Hive里面有配置可以设置

hive.parquet.timestamp.skip.conversion=true (default true)

Hive设置了该参数后,在读取Parquet文件的时候,同样会读取该Parquet文件的meta信息,如果是由Impala创建的Parquet文件的话,在读取的时候就不会进行时间格式的转换了。

Spark和Presto的Timestamp

Spark和Presto同Hive是兼容的,所以时间读取方面不会有问题。但是这样就意味着Spark和Presto就不能与Impala的时间相互兼容了,现在Spark和Presto似乎还没有和Impala相互兼容的解决方案。所以如果数据是由Impala来存储的,但是数据处理又是用Spark或者Presto的话,就要手动进行时区转换。

Parquet中的Timestamp

Parquet文件格式是当前Hadoop生态中最流行的列式存储格式。Parquet支持的类型有BOOLEAN、INT32、INT64、INT96、FLOAT、DOUBLE、BYTE_ARRAY,所以Timestamp其实是一种逻辑类型。由于Impala存储的时间精度达到纳秒的级别,所以在Parquet文件中用INT96来存储时间。其他的数据处理引擎也跟进该精度,所以也用INT96来存储,但是在时区兼容性方面做得并不好。

总结

不同的计算引擎之间的时间兼容性还是有一定的问题的,Hive和Impala之间有现成的解决方案来解决时间兼容性问题,但是Impala和Spark还有Presto之间暂时还没有好的解决方案,一个比较好的解决方法使用string来存储时间,这样不同的数据处理引擎之间就不会有时区问题了。

使用k8s部署Django项目

发表于 2019-01-13 | 分类于 python |

接触了一下docker和k8s,感觉是非常不错的东西。能够方便的部署线上环境,而且还能够更好的利用机器的资源,感觉是以后的大趋势。最近刚好有一个基于django的项目,所以就把这个项目打包到docker里面,放到k8是里面运行,顺便学习下k8s和docker的使用。

docker

为什么使用docker?

我觉得docker最大的好处是部署的时候比较方便,一个预先打包好的docker镜像,可以在任何安装有docker的机器上面直接运行,不用再安装其他任何的依赖环境。不管是在开发、测试、还是发布阶段,都能节省很多安装依赖和配置文件的时间,真正做到了Build once, Run anywhere。

docker在我的项目中怎么使用?

在我的项目中,我主要使用dockerfile来生成项目的镜像。我们都知道docker是按照层的思想来构建一个镜像的,我的镜像的最底层的操作系统使用的是centos7,再接着安装python相关的工具和库,然后安装项目所需求的库,最后再把项目拷到镜像中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM centos:7

ENV LC_ALL=en_US.utf-8 LANG=en_US.utf-8
RUN yum install -y https://centos7.iuscommunity.org/ius-release.rpm && \
yum install -y python36u python36u-libs python36u-devel python36u-pip mysql-devel gcc which && \
pip3.6 install pipenv

COPY Pipfile Pipfile.lock /my_app/
WORKDIR /my_app
RUN pipenv sync

COPY my_app_site /my_app/my_app_site
COPY gunicorn_config.py /my_app/gunicorn_config.py
COPY resource/nginx.conf /my_app/resource/nginx.conf

RUN mkdir /static/ && \
cd my_app_site && \
pipenv run python manage.py collectstatic && \
cd my_app_site && \
rm -f local_settings.py

WORKDIR /my_app

EXPOSE 8000
CMD pipenv run gunicorn my_app_site.wsgi -c gunicorn_config.py --log-file logs/gunicorn.log

在dockerfile中,每个RUN命令都会构建新的层,我这边之所以在dockerfile中使用三个RUN命令,是为了能够尽量的减少重复的构建过程。每次在构建镜像的时候docker都会判断每层的内容是否有修改,如果没有修改的话,就不需要重复的构建。所以在应用开发的过程中,上面的dockerfile最多也就重新构建最后一层和倒数第二层(在项目有新包加入的时候才重新构建倒数第二层,不然正常情况下就只会重新构建最后一层)。

k8s

为什么使用k8s?

k8s的功能非常强大。不过简单的来说,k8s是用来管理容器的一个工具。有了k8s以后我们就能让k8s自动的去拉取docker镜像,并且根据需要来启动、关闭、调度docker容器,实现一些牛逼的自动化运维操作。

k8s在我的项目中怎么使用?

我使用了yaml文件定义了一个k8s部署,下面是具体的文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# ------------------- MyApp Deployment ------------------- #

kind: Deployment
apiVersion: apps/v1beta2
metadata:
labels:
k8s-app: my_app
name: my_app
spec:
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: my_app
template:
metadata:
labels:
k8s-app: my_app
spec:
volumes:
- name: nginx-config
emptyDir: {}
- name: static-dir
emptyDir: {}
containers:
- name: my_app-web
image: my_app:latest
ports:
- containerPort: 8000
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d/
- name: static-dir
mountPath: /usr/share/nginx/html/my_app/static
command: ["/bin/sh"]
args: ["-c", "cp resource/nginx.conf /etc/nginx/conf.d/ && \
cp /static/* /usr/share/nginx/html/my_app/static -rf && \
pipenv run gunicorn my_app.wsgi -c gunicorn_config.py"]
- name: my_app-nginx
image: nginx:1.15.8
ports:
- containerPort: 8899
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d/
- name: static-dir
mountPath: /usr/share/nginx/html/my_app/static
livenessProbe:
httpGet:
scheme: HTTP
path: /
port: 8899
initialDelaySeconds: 30
timeoutSeconds: 30
---

# ------------------- MyApp Service ------------------- #

kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: my_app
name: my_app
spec:
ports:
- port: 8899
targetPort: 8899
selector:
k8s-app: my_app
type: NodePort

django项目在部署的时候需要用到nginx服务器,所以在部署的时候我在同一个pod里面也加入了一个nginx镜像,不过为了少打包一个新的nginx镜像(带有nginx配置文件和静态文件的nginx镜像),我在两个容器之间使用volumes来共享django静态文件和nginx配置文件。

总结

以上就是我使用docker和k8s来部署django项目的一个示例,文章里面没有包含一些具体的docker和k8s的介绍,是因为这些内容比较多,在这篇小文章里面放不下,感兴趣的同学可以去他们官网详细了解。最后我只想说,docker和k8s真是好东西,没有用过的同学赶紧去试试吧!

Impala性能优化

发表于 2018-11-24 | 分类于 离线处理 |

最近做了一些离线的etl,因为用到了kudu,所以大部分的etl都是在impala下面执行的。下面的性能优化是我在使用impala的时候总结的一些经验。

COMPUTE STATS

impala在做join的时候会根据表格的实际情况采用更优化的join方案,但是如果impala不知道表的统计信息(比如表格的大小)的话就不能定制优化的join方案,而compute stats命令就是用来让impala产生表的统计信息的命令。正常的情况下把所有要执行的表都执行下compute stats就能有最好的性能,但是有时候impala的优化也不一定是最好的,所以我们必须懂得自己分析执行的过程。

EXPLAIN

在执行sql语句之前可以使用explain命令来查看impala计划执行该sql的流程和预估消耗的资源。要优化sql语句的话最方便的就是改完sql语句后直接执行explain命令,因为该命令不用执行sql语句,可以很方便的调优sql语句,而不用任何的等待。在hue的执行窗口有explain的命令可以直接按,非常的方便。

SUMMARY

explain命令虽然在大多数的情况下是能够预估sql语句的执行情况,但是毕竟没有执行sql语句,所以预估的情况可能和实际情况会有一些出入。所以这时候我们可以使用summary,在一条语句执行完以后立马执行summary命令就能看到这条sql语句执行时所消耗的时间和资源的情况,还有impala预估的资源使用,有时候预估和实际情况还是有很多不同的。如下图所示的summary的预估内存和实际使用的内存就要较大的出入。当然如果想要看到更详细的执行信息还可以用Profiles命令,但是由于我在优化的过程中没有使用到,这边就不多说了。 impala summary 示例

SQL语句提示信息

通过上面的explain和summary后我们就能清楚的知道我们的sql语句的性能瓶颈在哪里,但是有时候即使执行了compute stats命令,impala还是不能优化这些性能瓶颈的问题,也就是说,impala的优化不能保证是最优的。所以我们就要告诉impala按照我们自己的想法来执行sql语句。比如:使用STRAIGHT_JOIN命令就可以告诉impala按照我写的sql语句的join顺序来执行join过程,而不进行重排(因为有时候重排的效果更差)。在join后使用-- +SHUFFLE注释就可以告诉impala使用SHUFFLE的方式进行join,而不是BROADCAST的方式进行join。我之前优化的一个sql语句因为加了-- +SHUFFLE注释而减少了好几G的内存使用量。

总结

这篇文章只是粗略的介绍下可以优化的点,具体的优化过程还是得结合自己的sql语句来定制自己的优化方案,如果想了解更多的优化技巧请移步impala官网的文档。

为集群搭建DNS服务的过程记录

发表于 2018-11-01 | 更新于 2019-01-17 | 分类于 教程 |

之前集群维护hostname和ip之间的映射关系使用的是/etc/hosts文件,但是由于每次新加机器的时候都要更新每台机器的/etc/hosts文件,比较繁琐而且容易出错,所以就考虑为集群增加dns服务。

搭建dns服务器

我们这边的dns服务器是运维直接帮我搭的,使用的是dnsmasq。相比于bind,dnsmasq的主要优势是轻量级、配置简单,然后我们机器的hostname和ip的映射文件放于这台机器的/etc/hosts文件下面,每次增加新机器的时候只要修改/etc/hosts文件,然后重启dnsmasq就可以了。

集群机器的配置

集群的机器配置主要是修改dns服务器的地址,把/etc/resolv.conf文件的内容修改为:nameserver 192.168.1.1(你自己的dns服务器地址)。
如果只是修改/etc/resolv.conf文件的话,centos7会在重启的时候重新生成该文件,把修改的内容覆盖。要解决这个问题的话需要在/etc/sysconfig/network-scripts/ifcfg-eth0文件中增加DNS1=8.8.8.8和DNS2=8.8.4.4类似配置,重启后/etc/resolv.conf文件的内容就会从ifcfg-eth0文件中生成。
在这步,我遇到了一个问题:用dig和nslookup可以找到一个hostname对应的ip,但是使用ping或者telnet的时候就会返回Name or service not known。我在网上查了很多的资料,有人遇到类似的问题,但是他们的问题是因为/etc/nsswitch.conf的host配置没配好,而我这边并没有该配置问题。最后我在一个帖子回答中找到了解决方法,原来是我启用了nscd服务,这样的话,修改dns服务器就需要把该服务重启下,重启完后问题就解决了。

配置优化

虽然经过上述的步骤后,集群的机器就可以使用dns服务了,但是还需要一些优化,才能让dns服务有更好的性能。

启用nscd服务

在centos7中默认是没有开启nscd服务的,这样的话,每次查询一个域名都需要向dns服务器发起一次请求,如果请求非常多的话就会给dns服务器带来压力,所以我们需要启用nscd服务。如果机器没有安装该服务的话,可以使用命令yum install nscd进行安装,然后使用命令systemctl start nscd启动该服务,使用命令systemctl enable nscd把该服务设置成开机启动。

配置dnsmasq的local-ttl参数

集群的机器启动了nscd服务以后,按道理对dns服务器的请求会大大的减少,但是我在dns服务器上使用tcpdump udp port 53查看集群机器对dns服务器的请求的时候,集群机器会一直重复的请求一个hostname的ip,nscd根本没有起到缓存到的效果,如下图所示: tcpdump显示log 后面查看dnsmasq的man手册知道,dnsmasq的local-ttl参数是控制本地域名的缓存时间的,比如存在/etc/hosts下的域名映射关系的缓存时间就是由local-ttl参数控制的,该值默认是0。我们集群的所有机器的hostname和ip的映射关系都是存储在dns服务器的/etc/hosts文件中的,所以这些域名在集群机器中是不缓存的,所以集群机器才会一直重复的请求某一个hostname的ip。最后为local-ttl配置一个适当值后该问题就解决了。

总结

为集群配置dns服务看起来是一件比较简单的事情,但是如果不知道其中的一些坑的话,配置起来就会挺费精力的,我就花了不少的时间。希望这篇文章能让其他人少走一些弯路。

使用django-guardian实现django-admin的行级权限控制

发表于 2018-10-27 | 分类于 python |

用django框架来做一些后台管理的web页面简直太方便了,django自带模块级的权限系统,用来做一些内部的系统非常合适,可以大大的减少开发量。但是django自带的权限系统还不支持行级的权限控制,如果要实现行级的权限控制,需要依赖第三方的app来开发,或者自己重新写一个。

需求描述

我们项目组开发的一些系统通常会用mysql数据库来存储一些配置,但是如果每次有配置修改的时候都去手动修改mysql数据的话,会挺麻烦的,同时也比较容易出错。django-admin能够根据定义的model自动的生成相应的页面,同时还能提供权限的管理,所以我们就把一些系统到的配置放到django中。但是到现在,随着系统的需求越来越多,该系统已经不止我们自己项目组的人员使用,也要开放给其他项目组的同事使用,所以就产生了一些更细粒度的权限需求。因此,我们要在现有的系统上支持行级的权限控制。

解决方案

当然可以自己写一套权限系统了,但是自己写的成本比较高,而且自己写的不一定比较好。所以我就先在网上找了一些现成的解决方案,https://djangopackages.org/grids/g/perms/ 该链接列出了现有的一些第三方的权限系统解决方案。从该页面来看,django-guardian是最受欢迎的第三方权限系统,而且支持行级的权限系统,同时还可以整合到django-admin里面,所以我就选择了django-guardian。

关键步骤

安装配置django-guardian

安装配置django-guardian比较简单,按照她项目提供的文档进行安装就可以了,安装完成后会在数据库里面创建两张权限相关的表。

把django-guardian整合到django-admin

首先把admin.py文件里面需要用到行级权限的类,由原来的继承admin.ModelAdmin,改成继承GuardedModelAdmin,这时候打开某个数据行的页面的时候,在该页面的右上角的历史旁边会显示编辑对象权限的按钮,点击该按钮进去相应的页面就可以编辑该行数据的具体权限。
配置完权限的时候,用一个新的用户测试的话,会发现该用户没有权限来访问任何的数据,这是因为GuardedModelAdmin还有很多事情没有帮我们做,我们还需要重写一些函数来实现admin后台页面的显示。具体的信息看下面的代码注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from guardian.admin import GuardedModelAdmin
from guardian.shortcuts import get_objects_for_user, assign_perm, remove_perm, get_users_with_perms, \
get_groups_with_perms

# 需改前
@admin.register(DataAssistantJob)
class DataAssistantJobAdmin(admin.ModelAdmin):
pass

# 修改后
@admin.register(DataAssistantJob)
class DataAssistantJobAdmin(GuardedModelAdmin):
# app是否在主页面中显示的话由该函数决定
def has_module_permission(self, request):
if super().has_module_permission(request):
return True
return self.get_model_objs(request).exists()

# 在显示数据列表额时候,哪些数据显示,哪些不显示,由该函数控制
def get_queryset(self, request):
if request.user.is_superuser:
return super().get_queryset(request)

data = self.get_model_objs(request)
return data

# 内部用来获取某个用户有权限访问的数据行
def get_model_objs(self, request, action=None, klass=None):
opts = self.opts
actions = [action] if action else ['view', 'change', 'delete']
klass = klass if klass else opts.model
model_name = klass._meta.model_name
return get_objects_for_user(user=request.user, perms=[f'{perm}_{model_name}' for perm in actions],
klass=klass, any_perm=True)

# 用来判断某个用户是否有某个数据行的权限
def has_perm(self, request, obj, action):
opts = self.opts
codename = f'{action}_{opts.model_name}'
if obj:
return request.user.has_perm(f'{opts.app_label}.{codename}', obj)
else:
return self.get_model_objs(request, action).exists()

# 是否有查看某个数据行的权限
def has_view_permission(self, request, obj=None):
return self.has_perm(request, obj, 'view')

# 是否有修改某个数据行的权限
def has_change_permission(self, request, obj=None):
return self.has_perm(request, obj, 'change')

# 是否有删除某个数据行的权限
def has_delete_permission(self, request, obj=None):
return self.has_perm(request, obj, 'delete')

# 用户应该拥有他新增的数据行的所有权限
def save_model(self, request, obj, form, change):
result = super().save_model(request, obj, form, change)
if not request.user.is_superuser and not change:
opts = self.opts
actions = ['view', 'add', 'change', 'delete']
[assign_perm(f'{opts.app_label}.{action}_{opts.model_name}', request.user, obj) for action in actions]
return result

通过上面的修改,django-admin中的模块就能够支持行级的权限,并能够正确的在后台页面中显示出来,当然如果有很多的模块需要支持行级的权限控制,则可以把上面的这些修改写到一个新的类中,然后其他想支持行级权限的模块再从该模块继承就可以了。

总结

感觉django-guardian和django-admin整合,实现的不是很好。如果开发者对django内部的代码不怎么了解,那么用django-guardian来实现行级权限控制的话会挺麻烦的,个人认为django-guardian完全可以把和django-admin的整合做到开箱即用的效果,就像django自带的权限系统一样。

写HDFS文件失败的问题处理

发表于 2018-10-27 | 分类于 错误处理 |

我们的集群在用fluentd写入hdfs的时候经常会遇到写入失败的问题,失败的特点是写入失败的那个文件的备份数只有2个(我们集群设置的是3个),然后就再也不能成功的往这个文件写入任何内容了。

问题描述

在集群负载比较高的时候,写hdfs文件有时会发生写入错误,写入错误的文件只有两个备份,该文件之后再也不能被写入了。

临时解决

之前还不知道产生这个问题的具体原因,但是出现了文件写入不进去的情况,又要及时的修复问题,我们的做法是先把该文件从hdfs上下载到本地,然后再把该文件从hdfs上删除,最后把本地的文件上传到hdfs中,这样就能把出问题的文件给修复好。
上述的解决方法,毕竟治标不治本,如果出错的文件比较少的话,还可以及时的修复,但是如果错误的文件数很多的话,那修复起来就非常的麻烦了。

彻底解决

产生该问题的原因

之前还不知道具体是什么原因造成的写入失败,后面在研究了hdfs写入过程的时候发现一个问题:那就是我们集群的机器数量比较少,只有四台机器,在写入hdfs文件pipeline的时候,如果有一台机器出现问题了时候,这时候就会把这台机器排除掉,去寻找新的可用的机器,但是我们的集群只有四台机器,如果刚好有两台机器出现问题的话,这时候文件的备份数永远也到达不了3。

解决问题的思路

解决问题的方法无非就下面几个:

  • 增加集群的机器数量。比如把集群的机器增加到10台,这样只有同时有8台机器出现问题的情况下,才会出现文件写入失败的问题。这样出现写入失败的问题的概率就会大大的减少。
  • 减少文件备份的数量。这个方法对我们不适用,因为我们现在使用的只有3个备份,这个应该是hdfs最低的要求了。
  • 修改配置,让写入的行为发生改变。dfs.client.block.write.replace-datanode-on-failure.enable配置默认是true,表示如果在写入的pipeline有datanode失败的时候是否要切换到新的机器,但是如果集群比较小的话,有两台机器失败的话,就没有其他机器可以切换了,所以把该配置设置成false后就能解决问题。
最后的解决方法

由于我们的集群不可能一下子就扩大,所以我们当前的解决方法是先把dfs.client.block.write.replace-datanode-on-failure.enable配置项设置成false,等后面集群变大后再把该配置项改成true。

总结

现在集群已经很久没有发生上述的错误了,说明配置的修改已经起到效果了。解决问题最重要的一步还是找到问题的原因,然后一次性解决掉,不然问题还是会时不时的光顾你的。

12…5
Darcy

Darcy

欢迎来到我的个人站

50 日志
17 分类
40 标签
RSS
GitHub E-Mail
© 2019 Darcy
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Mist v6.4.2