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

Sentry 新版插件基类讲解

根据 sentry/plugins/bases/__init__.py 的说明,新版插件分类四类,其中常见的:

  • NotificationPlugin 比如sentry_mail、sentry_webhook插件

NotificationPlugin的基类定义参见 sentry/plugins/bases/notify.py

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

Sentry整理杂记

本讨论均基于Sentry 7.7版本

插件机制

自带插件 src/sentry/plugins/ 每插件一个目录

自带插件loader:src/sentry/conf/server.py 里的INSTALLED_APPS tuple

外装插件loader:utils/runner.py 里的 install_plugins()函数,对iter_entry_points()遍历并将其加入INSTALLED_APPS tuple中

外装插件注册:插件的setup.py里执行注册entry_points的过程。例https://github.com/getsentry/sentry-groveio/blob/master/setup.py

参考setuptools:pkg_resources

列出本Python安装的Sentry插件
#!/usr/bin/env python
import pkg_resources
for ep in pkg_resources.iter_entry_points('sentry.apps'):
    print str(ep)
for ep in pkg_resources.iter_entry_points('sentry.plugins'):
    print str(ep)

 

sentry-jira插件

插件基本配置

每Project分别配置,需要输入jira的instance URL、用户名和密码。以上内容base64存在数据库sentry_projectoptions表里 where `key` like ‘jira%’

配置用户名密码之后,选择Sentry project关联到哪个JIRA Project,保存设置,并Enable Plugin即可。

 

Due Date问题

在Sentry Event页面右边点击Create JIRA Issue进入创建页面。但下面Due Date总提示Operation value must be a string 。

 点击展开故障详情

也就是说,如果不改sentry-jira插件,就无解。所以我提交了pull request https://github.com/thurloat/sentry-jira/pull/71

5日下午突然发现,目前线上Sentry 6.4.4的duedate显示为django的SelectDateWidget,而不像我自己的测试装Sentry 7.7一样直接用文本框。经过仔细对比,发现ops-sentry01上的sentry-jira插件是修改过的版本,forms.py文件class JIRAIssueForm新增了一段

site-packages/sentry_jira/forms.py片段
144
145
146
147
148
149
150
151
duedate = forms.DateField(
    label="duedate",
    #widget=adminwidgets.AdminDateWidget(),
    widget=SelectDateWidget(),
    initial=datetime.datetime.now(),
    #widget=DateWidget(usel10n=True)
    required=True
)

但这段代码没在公司的git库里保存,我就做了tree diff另外保存了。

SSO集成

厂家SSO讨论:https://github.com/getsentry/sentry/issues/1372

新版Sentry有auth backend基类https://github.com/getsentry/sentry/tree/master/src/sentry/auth/

SENTRY_FEATURES['organizations:sso']改为True可以开启Auth页面,设置sso。

目前,我参考sentry-sso-google写出来的sentry-sso-sankuai放在公司内网git库。

SENTRY_SINGLE_ORGANIZATION=True会导致/auth/login/ 跳转到 /auth/login/org_slug/ ,从而无法登录非SSO的用户(如系统自带的名为sentry的超级用户)。所以我在staging环境里关闭了这个参数。

Organizations

当SENTRY_SINGLE_ORGANIZATION=False时,utils/runner.py加载配置之后会将SENTRY_FEATURES[‘organizations:create’]强制改为False,从而禁用了右上角新创建Organization的“加号”链接。如果在此状态下删除了最后一个Organization,则其中的Team会变成游离状态,只能改掉参数重启服务重新创建org了,而且重建之后某种情况下会导致游离状态的Team丢失。切记不要删除Organization!!

根据getsentry原厂GH-1372号issue,每个Organization只能开启一个SSO AuthProvider。

目前看来,咱们的使用方法对多个Organization并没有需求。

 

新来的SSO用户默认属于所有Team的问题

经阅读代码文件web/frontend/accounts.py 发现在SENTRY_SINGLE_ORGNAZATION=True时会默认设置新注册用户的has_global_access值为True,然后如果有authprovider的话,再用authprovider.default_global_access更新该值;查看auth/helper.py 也发现会在登录和关联身份时,用 authprovider.default_global_access 给用户的has_global_access赋值。

2015年10月23日,将sentry_organizationmember表中 user_id in (35, 66, 67, 69, 71, 72, 73, 74, 77, 78, 81, 83, 84, 85, 86, 88, 89, 91, 92, 95, 96, 97, 98, 99, 100, 102, 103, 105, 107, 108, 109, 110, 112, 113, 114, 115, 117, 118, 119, 120, 122, 124, 125, 126, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 164, 166, 167, 168, 172, 173, 174, 175, 176, 177, 178, 180, 182, 183, 184, 185, 186, 187, 188, 189, 191, 192, 193, 194, 195, 196, 198, 199, 200, 203, 204, 205, 206, 207) 的111位用户的 has_global_access 值改为0,并将sentry_authprovider中id=2,provider=Sankuai的default_global_access字段改为0。

至于如何在SSO插件代码中设置,以便Sentry激活该SSO authprovider时自动将 sentry_authprovider 中 default_global_access字段设置为0,尚须进一步研究。将 orgnazation和authprovider关联的代码在web/frontend/organization_auth_settings.py 但其中并没有涉及default_global_access的内容,也许这个值并不是由authprovider影响的?

Team Admin无权批准人员加入的问题

这一代 Sentry 的权限系统设计的很烂,把 一个人属于哪个Team、一个人有权管理哪个Team混在一起了,无法表达“一个人是TeamA的member,同时是TeamB的admin”的意思。所以我一直倾向于授予较低的权限,但导致了事故。但10月29日由杨明芽发现 Team Admin 无法邀请、批准人员加入本Team,所以写了个后台任务定时批准requests。

Posted in 默认分类 | Tagged , | 1 Comment

SSO跳回sentry失败的解决方法

从某月开始,公司SSO回传信息改用POST方式,放弃了之前的通过querystring传递的做法。具体到Sentry这里,因为 /auth/sso 页面受CSRF保护,拒绝接受POST回来的不含CSRF token的数据,从而无法登录。

解决这个问题,可能有三种方法吧:

  1. 让SSO回传csrf token:前提是得先把csrf token发给SSO,且SSO方愿意配合修改。考虑到现在跳转到SSO去是直接302的,CSRF如果放在querystring上,其实和他们回传时把token放在querystring上风险相当了,所以这做法不行
  2. Sentry/auth/sso不验证CSRF:/auth/sso 对应AuthProviderLoginView,该View继承自BaseView,而BaseView的dispatch()方法是@method_decorator(csrf_protect)过的,因为还有其它很多View都这么继承,想改基类还怕有别的风险。想在配置文件里monkey patch掉AuthProviderLoginView,但是import sentry.auth.helper的时候就失败了
  3. Sentry整体不验证CSRF:试了试,csrf view middleware可以去掉,但是,csrf_protect decorator依然生效。关于此处,django文档说的不太精确。该middleware除了负责种cookie,还负责验证,但实际上验证工作并不是以middleware的身份来做的,而是以decorator的身份来做的。单单禁止middleware的加载只是去掉了种cookie的行为而已。网上也有其他人遭遇过这个问题 http://www.douban.com/group/topic/11555679/

最后,参照3里帖子的做法,做了一个django middleware插在csrf view middleware前面,如当前请求URI为/auth/sso/ 就取消CSRF保护。

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

记一次Sentry的性能调优过程

大概从6月底开始,我们的Sentry(错误日志收集、聚合和报警系统 http://getsentry.com )遭遇了性能问题,每分钟只能处理200个事件了,经常有20多万待处理的任务积压在events队列里,延迟超过一小时,我不得不丢弃这些任务,清空队列,以促进时效。重启一下celery worker会有瞬间的改善,但很快就又不行了,似乎worker的性能会衰减。为此,我给celery worker增加了–time-limit参数,使worker不会在执行不下去的时候无限等待,而是超时出错退出,迎接新的任务,情况略有改善。

因为Sentry的结构是前端web-消息队列-后端celery这样的结构,给调试也造成了一些困难。

我先开启了celery worker的DEBUG级别日志,从中搜索包含succeed的行,然后把其中UUID给过滤掉,只保留任务名字和耗时,整理后得到任务和耗时的对应关系,发现save_event这个任务的耗时很有意思,经常是6秒的整倍数。

然后去这个任务里添加时间打点代码,发现在它调用的EventManager.save()函数里,带事务执行的三次数据库插入(调用_save_aggregate、创建EventMapping、保存Event)的耗时,不是零点几秒,就是六秒的整倍数,而完全没有2、3秒、7、8秒这种情况。于是检查的方向就被引导到了数据库方面。折腾了DBA同事几天,删了几个索引和外键约束,也没什么改善。

想到之前为了Sentry的下一个版本v8准备过一套PostgreSQL数据库用于测试,我就拿过来,另外建了一套Sentry 7的部署,在上面运行,没想到性能极其丝滑……事情似乎越来越明了,就是数据库的问题。不过当我停止使用用于测试的128G内存真机PostgreSQL,而自己申请了一个和当前MySQL一样配置的16G机器的时候,发现低配置时pgsql的性能好像还不如MySQL……不过就在我从真实环境往测试环境export/import数据的时候,发现了Sentry在执行import时,如果测试环境共用生产系统的Redis,就会速度极慢,几乎卡死,用strace上去看一下发现慢在Redis操作上,开销在数据库上的时间极少;而如果用专用的一套Redis就速度极快。

于是我的注意力又转向了Redis。尝试FLUSHDB,发现性能立刻恢复了每分钟4000条、峰值8000条的处理能力,但redis的内存碎片率上涨了不少。看了看SLOWLOG,发现有一条几乎占满了整个SLOWLOG排行榜的命令

ZRANGE b:p 0 -1

手工试了试,这条命令得好几秒种才能返回,一次就能返回数据好几万条。搜了一下源代码,在sentry/buffer/redis.py文件发现了b:p这个key名字。原来,这是把高速更新的计数器的多次更新合并起来,减轻数据库压力的一个组件。但这个组件本身因为把sorted set用到了极限,所以性能不佳。考虑到不能再让碎片率上涨,我只好偶尔手工删除这个key,放弃一些计数器更新操作:放弃更新计数器总比放弃整个任务看起来要好些吧。

去软件原厂求助(https://github.com/getsentry/sentry/issues/3870),作者说我该上供啦,不要自己架服务啦,还是买原厂服务吧……

现在,我更改了worker的配置,把这个任务单独拉出来,用六台worker伺候着,终于基本解决了问题。

 

顺便说一下我在Redis 2.8.7上遇到的极限值:

sorted set的长度在100万左右就会性能崩溃。如果有依赖这个特性的,请大家注意有可能会引起整体系统的性能雪崩。

Posted in 默认分类 | Tagged , | 3 Comments

etcd的discovery URL是怎么回事

今天玩 CoreOS,得知他们提供了一个 etcd discovery service https://discovery.etcd.io/new?size=初始数量

当足够数量的etcd按照这个URL相互发现之后,就能成功组成集群。

问题是:

这个数量是怎么被各个etcd知道的呢?搜了一些文章,也谈到了自建 discovery service的方法,但URL似乎都是/v2/keys/random_ID/_config/size 这样的,而不像 discovery.etcd.io 这样是 /random_ID 。不过后来试了试访问 https://discovery.etcd.io/random_ID/_config/size 发现能得到那个值,就明白了,这个网址其实有rewrite,把 /random_ID 给proxy_pass到后面etcd的/v2/keys/random_ID 了,让这个URL看起来更友好,但却更迷惑了。

参考资料:https://github.com/coreos/etcd/blob/master/discovery/discovery.go#L214-L217

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

免签过境韩国、坐船去日本手记

我总有一种不愿意走回头路的情结,虽然给自己找了许多麻烦,依然乐此不疲。
这一次,经@tigerWu 提醒,我打算从韩国坐船去日本,以期获得一个日本的“上陆许可”。虽说日语里上陆就是入境的意思,但在汉语看来,有明确的从下到上的方向指示,也就是说得从海里上来才算。

去韩国驻华大使馆网站看看,中国公民持日本旅游非团体签证可以免签证过境韩国并给予30天入境期,查验标准是下一段“机票”。但我想坐船啊!那会不会是翻译有问题呢?要知道韩国大使馆上的汉语文件基本上行文都狗屁不通。再去看看韩国出入境管理局的网站,其英文版写了“flight ticket”。
这么看来是没戏了?我不甘心,就给“国民申闻鼓”写信,等不及回答又给出入境管理局的immigration contact center打电话,第一次问仁川机场进、釜山海港出,被拒了。不过比较有意思的是这个呼叫中心并不是直接做出行政解释,而是客人报告了自己的行程计划之后,她们代为向入境口岸当局咨询。考虑到中国对外国人的72小时免签证政策限定外国人不能离开口岸城市,我就尝试咨询釜山机场进、釜山口岸出,获得了允许,遂立刻买票,但为时已晚,只买到了没有头等舱的一架飞机的商务舱。(插播八卦:东航卖大韩航空的票比大韩自己卖的便宜)与此同时,我之前向韩国信访局“国民申闻鼓”递交的信访“是否可以空进海出,没指定具体口岸咨询”也收到了回答,我还到处求爷爷告奶奶找人翻译,后来发现其实有汉语翻译版的回答,就是:不限定转乘空路或海路。我把这一页打印出来了。

2月4日,大韩KE830,办理登机牌遇到了麻烦,地服代理请示大韩航空工作人员,我出示联程船票和信访记录之后拿到登机牌;中国边检出境,按规定应该查验联程客票,但都没理我就直接把中国出境章盖在了日本签证旁边。
到釜山机场,iPad丢了,于是落了单,在后面跟机组一起,机组走专用通道,我走外国乘客通道,是一位大妈入境官。入境官翻了半天,我主动提醒我是visa free transit资格,然后出示票。该女入境指着班次号问:这是flight吗?我说是ship,她拿不准,就问韩国人通道那个男入境官,然后我就被提到那个通道受审去了。因为JR九州beetle航运公司的船票确认页只写“釜山发”没说去哪儿,男入境官不能确认这是正确的票,我又重申了我从Busan international ferry
Terminal走,要去日本的Fukuoka,但他还是将信将疑,又问我是不是crew,我说不是之后,被拉出来去条凳上坐着(传说中的边检小黑屋?),入境现场指挥官,一位大妈过来问我,入境期间准备去哪儿玩,我说JSA(也就是板门店)她说板门店很远啊你知道吗,我说我做坐韩国高铁KTX,我有KR PASS,一种外国人专用的票。然后各位入境官扔下我不理,带着护照走了!!!这时船票在他们手里,信访函在我手里。过了一会儿,指挥官和男入境官把我带到入境通道,先盖章,然后手写了截止日期,又写了个不知道是TS还是TIS的字在入境章下面,这时还扣着,又说了我几句应该详细说明啊什么什么的,然后才用摄像头拍照,又说了我几句,最后主动和我握手,这事入境官的表情是很友好的。

2月8日,我提了行李到釜山火车站,经案内所工作人员指点,到站后去坐去往ferry的shuttle bus,但是没找到,只好打了个出租车去。一打吓一跳,原来google maps标的那个terminal现在已经不用了,新的在站后的左前方(东北),出租车到南边路口掉头从我到港口。巧的是,好像韩国人找零算法和美国人一样,出租车费2800、咖啡4800,我给整几千加三百,他们很难理解我是要一个500硬币找零。值得注意的是,港口的指示牌写boarding为“登机、搭乘”,写“便名”为“flight”,甚至有个指示牌写“快速船 登机口 flyer gate”我一直以为flight是飞机航班,而船班叫voyage呢!!

日本入境,边检极其顺利,其中问了我要去哪儿,我说福冈、仙台、东京,问我是飞过去吗,我说我有JR PASS,坐火车去,就给我贴了从海里上博多港陆地的“上陆许可”一枚,其实和上次大阪关西机场的只差下边的地名不一样。海关,又跟上次一样搜我的箱子,我长得有那么像走私犯吗?而且那个女关员还钓鱼执法,问我是不是住本地,我说不是啊我是中国人,然后就问那海关卡上那个地址是什么,我说那是hotel;然后又问我去哪些地方玩;最后甚至指着签证问这是什么,我说那是签证啊!!然后就过关了。他们是在考察什么?

结论:由于韩国出入境管理局各口岸部门执行政策不一致,目前这么走并不是一个很靠谱的方案,而且边检入境问话有时会有陷阱,比如问我是不是crew、问境内怎么交通之类的,稍有不慎就有可能被认为是有问题的。韩国边检把我扔在冷板凳好几分钟,谁知道是去打电话核实船票还是打电话请示政策去了……
《中国合伙人》教导我们:真实、自信、具体、合理,是签证的基本思想基本哲学,我的行程是真实的,就不怕核查、我的行程是符合信访回信解释的,也应该能进去。
不过我觉得,我得以自己的真实经历,去督促船运公司把票写清楚,不要只写起点站。

2月8日,写于博多

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