为什么有些系统是动作式的而非描述式的?

接手一个旧系统,上线发布是“拷贝文件过去,替换掉,重启服务”这样的,而不是“把编译好的东西打包,发上去安装,包的scripts部分带有重启动作”。

是否打包、包里包含什么,很有讲究:

  • 查询当前版本号:XXX -v vs rpm -qi XXX
  • 重启服务:killall XXX && sleep 30 && XXX vs /sbin/service XXX restart
  • 删除软件:rm -fr XXX vs rpm -e XXX
  • 安装依赖: ldd XXX |xargs -n1 yum provides |xargs yum -y install vs rpm -i XXX

……不胜枚举

描述式的好处是可以由机器自动评估效果,而动作式的效果需要人工评估。

描述式的好处的携带的信息(比如依赖关系)更丰富,而动作式的有可能在一开始就没收集齐足够的信息。

Posted in 默认分类 | Tagged | Leave a comment

placeholder 2016年10月欧洲之行

6月办签证,因为英国当时在闹脱欧,签证审批速度大大降低,耽误了办理申根,连瑞航机票都耽误了。所以决定10月辞职后再去。

临辞职,拿了在职证明去办法国申根,但忘记提前买好英法之间欧洲之星国际列车的票,后来多花了好多钱

走之前没查时区,10天行程调了5次时区,痛不欲生

不喜欢法国的繁复建筑物,但很爱她的博物馆们;塞纳河左岸下午照不到太阳,是酸臭文人吹捧起来的地方。

英国很有沉淀感

国际标准时间曾经是巴黎天文台,但英国人解决精度问题之后,标准时间变成了“比我巴黎时间晚9分9秒的‘那个时间’”

Posted in 默认分类 | Tagged , , | Leave a comment

django database router是个好东西

今天给sentry加了MySQL读写分离机制,记录一下:

DATABASES里,(sentry的脾气比较怪)保留名为default的配置,写master数据库的参数;新增一个名为slave的配置,使用只读用户名密码,或开启服务器端read only
然后增加一个类,带四个函数:

class DatabaseRWSplitRouter(object):
    def db_for_read(self, model, **hints):
        return 'slave'

    def db_for_write(self, model, **hints):
        return 'default'

    def allow_relation(self, model1, model2, **hints):
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if db == 'slave':
            return None
        else:
            return True

再把这个注册进去

DATABASE_ROUTERS = (DatabaseRWSplitRouter(), )

注意这个注册,可以写字符串形式的dotted_path也可以是一个对象,我偷懒就直接写了一个对象。

Posted in 默认分类 | Tagged , , | Leave a comment

内训教程释出:from CGI to WSGI

古代时,读过MSDN里的《from C++ to COM》受益匪浅。后来学习Python,自己也写了一篇 from CGI to WSGI 的教程,用于内部培训。

http://julyclyde.org/pyCGIWSGI.pdf

Posted in 默认分类 | Tagged | Leave a comment

给rq添加了RedisCluster支持

最近一直在和前同事Puff潘、RainSun吕合作,做一些给使用redis的软件增加redis cluster或者redis sentinel支持功能的工作。

传统上,做sharding工作有三种做法:

  1. 客户端支持,比如redis cluster客户端需要支持KeySlot计算(我报告的bug: https://github.com/Grokzen/redis-py-cluster/issues/153)
  2. 代理层支持:twemproxy、codis等
  3. 服务器端支持:比如MySQL partition table之类

我比较推崇的是第二种,但redis的cluster和sentinel都属于第一种,客户端需要有明确的能力和知识去处理连接多个服务器的问题,而这方面,各常见客户端库做的并不好。另外还有个问题是用Python语言写的各应用软件,往往直接写死了用redis库,而不是用rediscluster库或者运行期动态决定,导致根本不具备使用cluster的能力。

近期在看rq的时候,发现rq支持custom worker class,于是我就改了改,让它也支持custom connection class,并说服了作者merge进来。

https://github.com/nvie/rq/pull/741

不过其实我觉得还是略别扭。主要是因为生成connection对象的时候套了两层函数,为了补全function signature,并尽量减少其它代码的修改,不得不在两层都使用参数默认值,显得多余。尽管如此,还是很为自己能为基础软件做出贡献而感到高兴的。

Posted in 默认分类 | Leave a comment

kube-proxy –proxy-mode=iptables 与 rp_filter 冲突

2015年12月16日,朱鹏安装了新版kubernetes master版本(比1.1新,为1.2alpha**),然后发现,访问 clusterIP:clusterPort 会发生无法连接的故障。

从集群内Node上访问

分别在Node rz-ep19和container里执行curl访问,发现container里curl可以成功访问,但Node上一般不行(偶尔成功,几率很低)。查看iptables规则,发现新版kube-proxy已经不再将请求REDIRECT到本机kube-proxy端口,而是:

  1. 先把PREROUTING、OUTPUT无条件指向KUBE-SERVICES链;如匹配不上KUBE-SERVICES链,则再尝试匹配发给docker0
  2. 在KUBE-SERVICES链里匹配clusterIP:clusterPort条件,然后发到KUBE-SVC-***链
  3. 在KUBE-SVC-***链中,用-m statistic –mode random –probability这样的条件将流量按等比例分给多个KUBE-SEP-***链
  4. 然后再在KUBE-SEP-***链中,将数据包DNAT给endpoint

为了减少干扰,我们缩减了kube-system/elasticsearch-logging 10.16.59.73:9200服务的规模,到只有一个endpoint 172.16.86.48:9200运行在rz-ep10上;rz-ep19 10.16.49.16作为运行curl的客户端

  • 在rz-ep19的flannel接口上抓包,抓到了 10.16.49.16->172.17.86.48的TCP SYN,但没有收到回应。
  • 在rz-ep10(endpoint Pod所在的Node)的flannel接口上抓包,抓到了和上述相同的包,也没有收到回应。
  • 在rz-ep10的docker0接口上抓包,没有抓到

由此判断,rz-ep10的内核在转发时主动丢弃掉了 10.16.49.16->172.17.86.48的SYN,以至于无法建立TCP连接。查看/proc/sys/net/ipv4/conf/{all,flannel.7890}/rp_filter,发现flannel.7890/rp_filter内容为1,即在此网卡上执行“根据回溯路由检查数据包是否为伪造”的检查。因为源IP 10.16.49.16在rz-ep10看来理应出现自eth0而非flannel.7890接口,所以被判定为假造包,丢弃。

将此参数改为0,再去docker0上抓包,可以收到172.17.86.48发回10.16.49.16 SYNACK包;但rz-ep19上curl仍显示无法建立连接。

在rz-ep10的角度考虑,这个172.17.86.48->10.16.49.16的包应该从eth0发出,也就是在rz-ep19的eth0上收到。而在rz-ep19的角度考虑,源IP 172.16.86.48不应来自eth0,也会被rp_filter参数影响,丢弃掉,所以无法建立连接。把rz-ep19的eth0/rp_filter参数改为0,终于可以正常访问了。

 

plantuml9098040596798137974

 

从集群外访问

从办公区我的笔记本电脑 172.30.26.169 访问 10.16.59.67:80 服务,该虚IP被手工绑在rz-ep01上,ping可以通,但访问不通。

在笔记本电脑上抓包,发现只有从本机发往clusterIP的SYN包,没有返回,所以无法建立TCP连接。

改以Pod IP为过滤条件,发现Pod IP直接发回 SYN_ACK给我的笔记本电脑,但因为笔记本电脑这边没有发起对Pod的SYN,所以直接回复RST给Pod了。

改用iptables模式之后,由于不对称路径的问题,这种访问基本上无法以以前“把clusterIP绑在Node上”的做法实现

Posted in 默认分类 | Tagged | Leave a comment

Kubernetes内的网络通信问题

首先复习一下Kubernetes内的对象类型

  1. Node:运行kubelet(古代叫minion)的计算机
  2. Pod:最小调度单位,包含一个pause容器、至少一个运行应用的容器
  3. RC:复本控制器,用于保持同类Pod的并行运行的数量
  4. Svc:暴露服务的可访问通信接口

 

对象之间的通信关系

客户端
服务器
访问方式
master kubelet Node的10250/TCP端口。Node报到后,Master得知Node的IP地址
kubelet

&

kube_proxy

apiserver master:8080/TCP HTTP

配置文件写明master地址

kubectl命令行 apiserver localhost:8080/TCP或命令行参数指定
other ALL apiserver ClusterIP_of_kubernetes:443/TCP HTTPS

上述IP和端口号通过环境变量通知到容器

需要出示身份信息

kubernetes是一个ClusterIP模式的Service。参见下面详述

pod pod 跨Node的多个pod相互通信,需要通过overlay network,下面详述
ALL service 三种模式,下面详述

 

overlay network

kubernetes不提供pod之间通信的功能,需要装额外的软件来配合。我选的是出自CoreOS的flannel软件:

flannel是专门为docker跨Host通信而设计的overlay network软件,从ETCd获取配置,提供对docker网络参数进行配置的脚本,使用UDP、VXLAN等多种协议承载流量。

经实验,flannel在办公云(新)上会导致kernel panic

flannel配置

在/etc/sysconfig/flanneld 配置文件中写好etcd的地址

用etcdctl mk /coreos.com/network/config 命令将下列配置写入etcd:

{
"Network": "172.17.0.0/16",
"SubnetLen": 24,
"Backend": {
    "Type": "vxlan",
     "VNI": 7890
 }
 }

Network代表flannel管理的总的网络范围;SubnetLen是其中每个节点的子网掩码长度;Backend规定了各节点之间交换数据的底层承载协议。

再各Node执行 systemctl start flanneld 启动服务。

 

flannel对docker作用的原理

flannel 并非“神奇地”对docker产生作用。

查看/usr/lib/systemd/system/flanneld.service配置文件可知,在flanneld启动成功之后,systemd还会去执行一次/usr/libexec/flannel/mk-docker-opts.sh脚本,生成/run/flannel/docker环境变量文件。

flannel的RPM中包含的 /usr/lib/systemd/system/docker.service.d/flannel.conf  ,作为docker服务的配置片段,引用了上述环境变量文件 /run/flannel/docker ,故 flannel 必须在 docker 之前启动,如果在docker已经运行的情况下启动flannel,则docker也必须重启才能生效。。通过 systemctl status docker 可以看到

# systemctl status docker
docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled)
  Drop-In: /usr/lib/systemd/system/docker.service.d
           └─flannel.conf
   Active: active (running) since 一 2015-12-14 15:53:06 CST; 19h ago
     Docs: http://docs.docker.com
 Main PID: 149365 (docker)
   CGroup: /system.slice/docker.service
           └─149365 /usr/bin/docker daemon --selinux-enabled -s btrfs --bip=172.17.33.1/24 --mtu=1450 --insecure-registry docker.sankuai.com

flannel作用于docker,最终表现为设置了其–bip 参数,也就是 docker0 设备的IP地址。如重启时失败,可以用ip addr命令删除docker0上的IP然后再重新启动docker服务。

另外,flannel会在各节点上生成一个名为flannel.7890的虚拟接口(其中7890就是上面配置文件里的VNI号码)其掩码为/16,但IP和docker0的IP地址相近。这种配置生成的路由表如下:

即:与172.17.33.0/24 通信,通过docker0;与172.17.0.0/16 通信(不含172.17.33.0/24),通过flannel.7890 发往overlay network

Docker和容器的网络

用ip link add vethX peer name vethY命令添加一对虚拟以太网接口。

veth和普通eth接口的区别在于:veth一次就要配置一对,这一对veth接口有背对背的隐含连接关系,可以通过tcpdump和ping来验证。

然后把其中一个网卡移到容器的namespace里,另一个加入docker0 bridge,即完成将容器连接到docker0 bridge的工作。

查看namespace内情况的方法,请参见https://gist.github.com/vishvananda/5834761

Kubernetes Pod的网络配置

一个Pod最少包含两个容器,其中一个叫pause,它什么都不敢,只“持有”IP地址。通过docker inspect可以看到:

[
{
    "Id": "2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
    "NetworkSettings": {
        "Bridge": "",
        "EndpointID": "2c1e17f3fd076816d651e62a4c9f072abcc1676402fb5eebe3a8f84548600db0",
        "Gateway": "172.17.33.1",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "HairpinMode": false,
        "IPAddress": "172.17.33.2",
        "IPPrefixLen": 24,
        "IPv6Gateway": "",
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "MacAddress": "02:42:ac:11:21:02",
        "NetworkID": "6a23154bd393b4c806259ffc15f5b719ca2cd891502a1f9854261ccb17011b07",
        "PortMapping": null,
        "Ports": {},
        "SandboxKey": "/var/run/docker/netns/2b36d6cb2f2d",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null
    },
    "HostConfig": {
        "NetworkMode": "default",
        "IpcMode": "",
    }
]

而真正运行应用的那个容器,网络配置则是这样:

[
{
    "Id": "eba0d3c4816fd67b10bcf078fb93f68743f57fa7a8d32c731a6dc000db5fafe8",
    "NetworkSettings": {
        "Bridge": "",
        "EndpointID": "",
        "Gateway": "",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "HairpinMode": false,
        "IPAddress": "",
        "IPPrefixLen": 0,
        "IPv6Gateway": "",
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "MacAddress": "",
        "NetworkID": "",
        "PortMapping": null,
        "Ports": null,
        "SandboxKey": "",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null
    },
    "HostConfig": {
        "NetworkMode": "container:2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
        "IpcMode": "container:2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
    }
]

注意对比NetworkMode和IpcMode的值,后者的NetworkMode和IpcMode的值为前者的Id。

Kubernetes这种设计,是为了实现单个Pod里的多个容器共享同一个IP的目的。除了IP以外,Volume也是在Pod粒度由多个容器共用的。

Kube-Proxy服务

kubernetes各节点的kube-proxy服务启动后,会从apiserver拉回数据,然后设置所在机器的iptables规则。

对于如下的svc:

# kubectl describe svc/mysql
Name: mysql
Namespace: default
Labels: name=mysql
Selector: name=newdeploy
Type: ClusterIP
IP: 10.16.59.77
Port: <unnamed> 3306/TCP
Endpoints: 172.17.29.35:3306
Session Affinity: None
No events.

生成的iptables规则如下:

-A PREROUTING -m comment –comment “handle ClusterIPs; NOTE: this must be before the NodePort rules” -j KUBE-PORTALS-CONTAINER
-A PREROUTING -m addrtype –dst-type LOCAL -m comment –comment “handle service NodePorts; NOTE: this must be the last rule in the chain” -j KUBE-NODEPORT-CONTAINER
-A OUTPUT -m comment –comment “handle ClusterIPs; NOTE: this must be before the NodePort rules” -j KUBE-PORTALS-HOST
-A OUTPUT -m addrtype –dst-type LOCAL -m comment –comment “handle service NodePorts; NOTE: this must be the last rule in the chain” -j KUBE-NODEPORT-HOST

-A KUBE-PORTALS-CONTAINER -d 10.16.59.77/32 -p tcp -m comment –comment “default/mysql:” -m tcp –dport 3306 -j REDIRECT –to-ports 53407
-A KUBE-PORTALS-HOST -d 10.16.59.77/32 -p tcp -m comment –comment “default/mysql:” -m tcp –dport 3306 -j DNAT –to-destination 10.16.59.13:53407

(之所以在PREROUTING和OUTPUT设置相同的规则,是为了匹配从外部来的访问和发自本机的访问两种情况)

上述规则把访问 10.16.59.77:3306/TCP的请求,转到了本机的53407端口。用lsof检查可以发现该端口为kube-proxy进程。kube-proxy在各机器上会选择不同的端口以避免冲突。kube-proxy收到请求后,会转发给Service里定义的Endpoints

1.1版本开始,新增–proxy-mode=iptables模式,直接按相同比例把请求DNAT到指定的endpoint去。

Kubernetes Service的三种模式

  1. ClusterIP模式
  2. NodePort模式
  3. LoadBalancer模式

ClusterIP模式

生成一个 只在本cluster内有效的IP:port 组合,也就是仅对内暴露该服务。生成的IP范围由apiserver的–service-cluster-ip-range参数规定。

将生成的IP绑在一台运行着kube-proxy的机器上时,也可以对外提供服务。1.1版本的kube-proxy –proxy-mode=iptables时不能支持将clusterIP绑在Node上对外服务的做法。

NodePort模式

在所有Node上,用相同的端口号暴露服务。如Node的IP对cluster以外可见,则外部也可以访问该服务。

LoadBalancer模式

通知外部LoadBalancer生成 外部IP:port组合 ,并将请求转发进来,发给NodeIP:NodePort们。该行为听起来会把数据转发很多次。

该功能需要外部环境支持。目前GCE/GKE、AWS、OpenStack有插件支持该模式。

Posted in 默认分类 | Tagged | Leave a comment

Docker in docker的一些故障检查过程

术语约定:

  • Host:外层运行操作系统的机器
  • 外层daemon:Host上的docker daemon
  • 外层容器:外层daemon下辖的container,镜像启动时加–privileged参数。这个镜像的准备步骤是从docker下载当前1.9.1版安装(并固化到镜像里)CMD是一个脚本,先启动带debug选项的docker daemon 并放后台运行,然后pull并运行centos:7 一次,最后开启一个不停ping的命令,保持容器持续运行。通过docker exec 进入另行执行docker run命令测试内层是否可以正常启动
  • 内层daemon:外层容器里的docker daemon
  • 内层容器:内层daemon下辖的container

 

宋传义最近几周在尝试docker in docker,报告过几个问题,我在这里简要记录一下。因为在此docker in docker研究过程中我只是顾问的身份,并非主研人员,所以记述内容难免有缺乏背景介绍、阶段靠后等问题。宋传义报告的大量现象都是“最后一句错误信息”,但我的工作方式是从“第一条错误信息开始看”。

启动内层docker daemon时报告缺cgroup mount

宋传义报告在1.9上可以成功的在外层容器里运行内层的docker daemon,但1.7的报告缺cgroup mount。检查发现,Docker 1.7 并不会给内层容器 mount /sys/fs/cgroup/* 目录。只需要手工补mount即可混过去,满足启动docker daemon的需求。

在Docker 1.8.0的changelog里 Runtime 小节记述了这个变动:

  • Add cgroup bind mount by default

因此1.8.0以后都是可以的。CentOS 7.1.1503 早先版本带的是 1.7.1 后续升级到 1.8.2。后经催促,公司内网的安装源更新到新版本 。

不过1.8.2 RPM的docker-storage-setup脚本有问题 https://bugs.centos.org/view.php?id=9787 在未启用LVM的情况下会直接报错退出,无法从 /etc/sysconfig/docker-storage-setup 生成 /etc/sysconfig/docker-storage 配置文件。所以建议手工维持这俩文件的一致性。

在外层容器里启动内层容器时报告缺/sys/fs/cgroup/docker.service

这个故障,宋传义描述为“只有rz-ep17上docker in docker运行正常,其它机器均失败”。我尝试了一下,其它机器也不是全都失败,只是失败概率极高,偶尔还能遇到stack overflow;rz-ep17也不是每次都成功,但成功率极高。宋传义报告的故障现象为 docker run 失败,错误信息为 umount shm 和 umount mqueue失败。

首先双人交叉检查故障机和正常机的软件版本,发现Host内核、Docker外层daemon版本均精确一致、命令行精确一致;内层docker不管什么版本都能重现故障。听起来似乎是灵异现象。

 

尝试用fatrace、inotify-tools检查,发现fatrace在打开fanotify之后,IO事件发生后即收到File too large错误信息退出;而inotify直接就没动静。看起来这俩工具还不兼容container环境。

 

scytest 这个镜像启动时会在后台启动 start_docker.sh 它会在后台运行内层daemon。

在这个daemon环境下,用 docker run -ti 启动内层容器,则基本可以确保损毁当前运行的内层docker daemon,后续所有次数启动内层容器均会出现umount shm和umount mqueue失败的问题。后续我们发现是上次daemon出错时未能及时umount掉device-mapper设备,虽然下次daemon启动时会尝试清理,但还是没清理干净。手工umount掉/var/lib/docker/container/***ID***/{shm,mqueue} 即可修复。期间还尝试在外层容器里执行dmsetup remove_all 结果发现删除了容器里的device mapper之后,Host的device mapper设备节点漏进来了,是个安全隐患。

在这个daemon环境下,用 docker run -d 启动内层容器则大概率会成功。

如果kill掉start_docker.sh启动的docker daemon,手工在docker exec bash的命令行上另启动一个daemon,则一定出/sys/fs/cgroup/docker.service的问题。

于是问题逐渐清晰起来。

 

错误信息的文件名 docker.service 看起来“比较像systemd的命名风格”,所以我找了一下,发现在Host的cgroup目录里 /sys/fs/cgroup/systemd.slice/有个docker.service目录,但外层容器内的cgroup并没有这个。

搜源代码也搜不到docker.service 这个字符串,于是只能判定为该路径是从外部获取并拼装起来的。考虑到命令行精确一致,我又去看了看环境变量,也没有发现相关内容。

 

凝神定志,用重量级武器strace -f 跟踪内层docker daemon,记录下其文件访问行为,并比对错误信息,可以清晰的看到准备容器文件系统内容、mount、准备容器的cgroup环境、运行程序、失败、清理现场的过程,而且发现对 /sys/fs/cgroup/docker.service 的访问是由 内层daemon调用native exec driver 执行的,还未运行到启动容器内程序的步骤,也就是说,访问的是 外层容器内的/sys/fs/cgroup/docker.service 而不是 内层容器内的该文件。native exec driver目前是libcontainer/runc的一部分。我去GitHub搜源码,偶尔看到在https://github.com/opencontainers/runc/blob/3317785f562b363eb386a2fa4909a55f267088c8/libcontainer/cgroups/utils.go 中有分别读 /proc/1/cgroup 和/proc/self/cgroup 的两个函数,顿时敏感的意识到root cause就在这里。我赶紧去核对,发现 从CMD/ENTRYPOINT启动的start_docker.sh及其子进程docker daemon、子进程ping的/proc/<PID>/cgroup内容最后一行,和手工docker exec 进去执行的所有命令的该文件的最后一行内容不同:

image2015-12-4 8-40-34

上述路径前面拼上/sys/fs/cgroup/systemd/即为Host上的路径。

看起来应该是由于docker run设置了容器的cgroup环境,所以容器内原生的进程都基础此设置;而docker exec没有这个初始化过程,只是直接送一个进程在容器里执行,所以不同。

根据这个结论,宋传义进行了回归测试,终于可以100%重现失败过程,近100%重现成功过程(部分失败由于代码质量引起stackoverflow)

启动内层容器时报告缺/sys/fs/docker-daemon

错误信息 Error response from daemon: Cannot start container 8aa10e0596282a11b7d841f25355426e9a5e395cb980cf66ec89c9d1a439ae4d: [8] System error: mkdir /sys/fs/docker-daemon: no such file or directory

和上面那个故障相关

image2015-12-4 11-53-53

但因为mkdir: cannot create directory ‘/sys/fs/docker-daemon’: No such file or directory (/sys/fs/ 不是一个tmpfs而是/sys/的一部分;对比/sys/fs/cgroup/ 是个tmpfs可以随便写入)所以此问题无解

奇怪的是,我手工启动一个 daemon 其状态如下:

image2015-12-4 12-2-14

结果一样,还是出docker-daemon目录的错误。

可能这就是宋传义在CMD docker daemon和EXEC docker daemon之间来回切换的原因吧?

 

重启rz-ep16,然后查看,Host上docker服务刚启动时cgroup为

10:hugetlb:/
9:perf_event:/
8:blkio:/
7:net_cls:/
6:freezer:/
5:devices:/
4:memory:/
3:cpuacct,cpu:/
2:cpuset:/
1:name=systemd:/system.slice/docker.service

启动一个–privileged容器之后变成

10:hugetlb:/
9:perf_event:/
8:blkio:/system.slice/docker.service
7:net_cls:/
6:freezer:/
5:devices:/system.slice
4:memory:/system.slice
3:cpuacct,cpu:/system.slice/docker.service
2:cpuset:/
1:name=systemd:/system.slice/docker.service

这个看起来和一直伟光正的rz-ep17相同,且随后的实验都完全成功。

经实验,发现docker被kubelet依赖启动的时候,/proc/<PID>/cgroup 文件中perf_event、freezer、cpuset三行会是/docker-daemon;docker独立启动时则为/ 这样的正确内容。但这俩服务有强关联:systemctl restart docker重启还是错误内容;systemctl stop再start docker成功,但会导致kubelet服务停止。

外层容器首次yum会失败

image2015-12-4 12-41-40

稳定重现,原因不明。第二次就没事了

结论

  • 看错误信息要看第一条,而不是最后一条
  • 运维相关工具是检查不熟悉程序的行为的利器
  • 容器内和操作系统上的运行环境差异较大,除了fatrace\inotify失败,以后可能还会遭遇其它兼容性问题
  • Docker的坑还很多,尚处于初期开发阶段,变动很大,质量较差
  • 我们对 cgroup 的认识还太粗浅
  • 我们对devicemapper完全无认知
  • 编译型语言调试比较困难
Posted in 默认分类 | Tagged , | Leave a comment

Sentry新版SSO Provider讲解

从GH-1372 issue完成时开始,Sentry 7.x转向使用自家定义的SSO Provider,逐渐抛弃django-social-auth结构。因为缺乏文档,我在此事上消耗了大量的时间。现在写这篇Wiki用于记录:

基类:sentry/auth/provider.py 中的 Provider 虚基类

样例:sentry/auth/providers/ 目录下的dummy.py和oauth2.py 两个,以及sentry-sso-google 基于Sentry自带的OAuth2Provider

 

基本执行流程:

  1. 通过django机制加载app,在初始化时调用sentry.auth.manager的register方法,注册自己的名字和class
  2. 管理员在 Organization 的Auth页开启SSO时,会生成上述类的一个实例。Sentry的helper.py负责此流程,先调用get_auth_pipeline()函数,取得一个成员为多个 AuthView的超类 的list,然后对着这个list依次执行成员的 dispatch()函数
  3. 每个步骤按照pipeline的顺序执行一个AuthView.dispatch(self,request,helper)函数,其中request可以读到当前的django request内容。因为URL都为/auth/sso/ 所以需要根据querystring来判断执行到哪一步了,并收集本步的信息,调用 helper.bind_state() 保存起来,然后return help.next_step()跳到下一步执行
  4. 最后build_identify()会被执行,需要返回一个至少包含id、email、name的dict,此dict被Sentry用来构造用户信息。
Posted in 默认分类 | Tagged , | Leave a comment

sentry-公司内IM插件

这插件被大家催了很久了,到1月底终于决定动手做。

准备知识

公众号Pub服务API(内网wiki,链接已删除)

签名请求规范(内网wiki,链接已删除)

New Forms of Authentication

插件组成

  • setup.py 安装脚本。插件注册发现机制请参见
  • sentry_IM/__init__.py 确保这个目录是python package的文件。顺便查询一下VERSION供引用
  • sentry_IM/requests_APISign.py 上述New Forms of Authentication文档里提到的requests auth class,提供公司的Authencation验证header
  • sentry_IM/send_IM.py 发消息用的Class
  • sentry_IM/plugin.py 插件本身

程序讲解

插件本身继承自sentry.plugins.bases.notify.NotificationPlugin类,需要实现其notify(self, notification)方法。因基类里notify()调用了notify_users(),所以pylint会有提示。

参数notification为一个对象,可以从中取得event,然后从event取得所属的group以及group所属的project。根据project可以取得要通知的人员列表。(这里抄袭了MailPlugin的发送列表)因为这个函数得到的人员列表是Sentry里的User id,所以还需要再根据id查询到User,然后从其Email字段中取得localpart

最后,根据上述project.name、event.message、group.get_absolute_url() 组装其通知消息文本(URL在IM客户端渲染时才变成超链接,我这里只发URL本身),生成send_IM.py 的实例,对着上述人员列表发送消息文本。

IM插件所需的四个参数,在总的配置文件中列出,由插件生成send_IM实例时传递,然后由其传递给requests_APISign的实例,完成对HTTP请求的签名

配置是否开启

本来是想像MailPlugin那样直接在Account/Notification页做个列表供勾选。但阅读代码发现,这个功能并不是MailPlugin自己的功能,而是sentry做了特殊处理,先获得用户订阅的projects_list,然后循环生成ProjectEmailOptionsForm的list,随后再执行从各插件获取的ext_forms[]配置表单。ext_forms里每一个插件配置表单的初始化参数只有plugin、user和prefix,要想表达“所有可订阅的projects”这个概念会相当啰嗦。因此,最终确定为:表单里只勾选per-User 级别是否开启;而用户是否属于具体某个event/project的发送目标,则直接共用MailPlugin那边的选择,然后再用上述per-User级别开关做一遍过滤。这里两个插件之间略有粘连,主要是由于User-Project之间只有单向对应关系,逆向查询太罗嗦导致的。

image2016-2-3 16-17-40

最终发送效果

image2016-2-3 16-19-56

Github地址 https://github.com/julyclyde/sentry-IM

Posted in 默认分类 | Tagged , | Leave a comment