先说说背景,为什么会要了解一下/etc/resolv.conf配置,起因是一个跑在k8s集群的一个业务出现问题,仔细排查后,发现其中一个Pod的域名解析有问题,域名login.example.com被解析到了一个IP,而这个IP地址是另一个范域名*.ichenfu.com的解析,经过一番调查,最终发现是同事在配置一台机器上的kubelet时填错了clusterDomain的配置,将原本需要配置为c2.ichenfu.com的配置写成了c1.ichenfu.com,那么问题来了,为什么这么配置会导致DNS解析到一个错误的,而且是完全不相干的地址的呢?下面就慢慢分析一下。

首先还原一下场景,默认情况下,kubelet启动Pod的时候,会将DNS配置注入到Pod中,出问题的Pod里/etc/resove.conf内容如下:

1
2
3
nameserver 10.254.0.2
search default.svc.c1.ichenfu.com svc.c1.ichenfu.com c1.ichenfu.com localdomain
options ndots:5

阅读全文

这两天因为内部kubernetes的网络配置问题和同事交流了一下,由于内部使用了calico网络,在内部pod出网时有两种选择,使用nat或者不使用nat,为此还经历了一番讨论,突然发现自己对netfilter包括其相关的很多概念还是比较模糊,所以查了查资料,尝试深入了解一下。

netfilter

在网上找到了一张图,发现还是能比较清楚的描述整个netfilter架构的,来源来自http://xkr47.outerspace.dyndns.org,先把图贴出来:

netfilter packet flow

这张图更像是从iptables chain的角度去描述netfilter数据流,总的来说其实不太影响最终的理解,实际netfilter提供了NF_IP_PRE_ROUTINGNF_IP_LOCAL_IN
NF_IP_FORWARDNF_IP_LOCAL_OUTNF_IP_POST_ROUTING几个HOOK点,具体到图上:

- `PREROUTING`: 对应`NF_IP_PRE_ROUTING`,看名字就可以知道,该HOOK在收到数据包,进行路由判断之前触发;
- `INPUT`: 对应`NF_IP_LOCAL_IN`,当经过`PREROUTING`阶段,如果目的地址是本机,那么将触发`INPUT`,之后就可能被传给应用程序处理;
- `FORWARD`: 对应`NF_IP_FORWARD`,对应如果数据包在路由表中是需要转发到另一个网络接口的,那么将触发`FORWARD`;
- `POSTROUTING`: 对应`NF_IP_POST_ROUTING`,所有数据包在进行路由选择之后,在实际发送给网络接口之前,会触发`POSTROUTING`;
- `OUTPUT`: 对应`NF_IP_LOCAL_OUT`,对于所有本地生成的数据包,在路由选择之前会触发`OUTPUT`。

PS:根据文档描述,包括上图中的备注也说明了,在实际上,对于本地生成的数据包,是先进行过一次路由选择,拿到一些需要的信息(比如源IP和一些IP选项)后,再触发`OUTPUT`的。

实际上netfilter最重要的就是提供这些HOOK点,针对图上的这些HOOK点,可以方便的注册各种处理逻辑来实现对包的处理,像常用的LVS,也是利用了这一系列的HOOK,来实现负载均衡功能。

iptables

说完netfilter的基本信息,需要在具体说一下iptables的主要数据流,实际上iptables也是在netfilter上注册了一系列的HOOK,并将这些HOOK通过几个table来管理,同样是针对上面的图,从iptables table这个角度来看,
也可以很直观的看到iptables的所有表,到底都在netfilter的哪些阶段被注册了,在很的教程中,都喜欢以table维度来介绍数据流,个人觉得是没有从hook这个维度看起来清晰的。

需要说明的是,因为NAT包含SNAT(修改源地址)和DNAT(修改目的地址),而这两种NAT发生作用的时间也是不一样的,在图上可以看到,DNAT发生在NF_IP_PRE_ROUTINGNF_IP_LOCAL_OUT阶段,
而SNAT发生在NF_IP_POST_ROUTING阶段,其实也很好理解,仔细想想就可以知道为什么是这样了。

不过对于上图里,和实际不对应的地方,iptables的SNAT其实也是可以在INPUT里实现的,而图上并没有画出来。

Connection Tracking

最后再说一下Connection Tracking(连接跟踪),连接跟踪也是在netfilter上实现的,可以给iptables提供在连接的各个阶段对数据包进行操作的能力,也就是可以提供一个跟状态挂钩的服务(毕竟TCP链接是有状态的)。
数据包进入网络栈后,只经过一些基本的检查,以及raw表操作之后,很快就会被连接追踪给追踪了,根据收到的包,可以根据实际情况针对性的修改追踪中的各种链接状态,当然连接追踪也是可以跳过的,只需要在raw表中操作数据包将数据包添加NOTRACK标记,那么连接跟踪将会不处理数据包和其连接。

整体内容不是很多,也没有非常深入去了解所有的机制,特别是代码方面,但是一张图还是能提供非常多的信息,特别是对整体的架构了解帮助很大,具体到更多的应用,就额外再进行记录吧~

参考:

  1. https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
  2. https://www.netfilter.org/documentation/HOWTO//netfilter-hacking-HOWTO-3.html

Harbor是vmware中国开发的一款企业级的DockerRegistry服务器,我们内部也是有搭建了一个Harbor,但是版本是0.5,对于当前最新的release版本1.5.2而言已经太老了,确实也有一些问题,比如不支持多级的镜像名称,某些情况下会触发bug导致panic。

所以需要升级一下,既然考虑升级了,就干脆升级到最新的版本1.5.2了。首先说一下目前的Harbor,官方提供的离线安装包里,默认是本地启动一个MySQL,将Harbor需要的一些数据存储在本地的MySQL中的,这个是不能接受的,所以在之前的部署中,是使用了外部的一个MySQL,同样,registry的存储在线上也是使用了共享存储,保证可用性。

不过Harbor的1.5.2版本对于0.5版本变化还是比较大的,首先是增加了adminserver这个角色,将所有的配置都拿到adminserver中存储,ui组件通过http请求定期向adminserver请求当前最新的配置信息,其次是数据库结构,新版本和旧版本相比数据库结构发生了很大的变化。

对于升级操作,官方也提供了解决方案,可以参考migration_guide进行升级,升级工具的镜像官方也是提供了,但是这其中存在一个问题,就是升级工具依赖本地的MySQL,也就是说,这个工具只能工作在MySQL是Harbor离线安装包启动的情况下,如果使用了外部的MySQL,这个升级工具就无法直接使用了。

所以呢,最终还是需要去看一下官方的升级工具是如何实现的,看能否通过其他办法手动升级,于是就花了点时间看了一下代码,找到了最后的实现方式,具体的代码在alembic/mysql这个目录下,原理也很简单,官方使用了一个Python的工具alembic实现了数据库结构的版本管理。

手动运行数据库升级,首先需要安装alembic工具,可以通过pip安装,或者针对不同的发行版找对应的软件包。

下面开始操作:

阅读全文

最近公司有个Ceph集群出了点问题,于是也参与了修复的过程,过程中最让人头疼的就是一堆不明所以的状态了,所以看了看文档,也找了一些参考,
整理了一下Ceph PG的一些状态以及相关的概念说明,做了一个中英文的对照版本:

Placement Group States(PG状态)

当检查一个集群的状态时(执行ceph -w或者ceph -s),Ceph会汇报当前PG的状态,每个PG会有一个或多个状态,最优的PG状态是active + clean
下面是所有PG状态的具体解释:

creating

Ceph is still creating the placement group.
Ceph 仍在创建PG。

activating

The placement group is peered but not yet active.
PG已经互联,但是还没有active。

active

Ceph will process requests to the placement group.
Ceph 可处理到此PG的请求。

clean

Ceph replicated all objects in the placement group the correct
number of times.
PG内所有的对象都被正确的复制了对应的份数。

down

A replica with necessary data is down, so the placement group is
offline.
一个包含必备数据的副本离线,所以PG也离线了。

scrubbing

Ceph is checking the placement group metadata for inconsistencies.
Ceph 正在检查PG metadata的一致性。

deep

Ceph is checking the placement group data against stored checksums.
Ceph 正在检查PG数据和checksums的一致性。

degraded

Ceph has not replicated some objects in the placement group the
correct number of times yet.
PG中的一些对象还没有被复制到规定的份数。

inconsistent

Ceph detects inconsistencies in the one or more replicas of an
object in the placement group (e.g. objects are the wrong size,
objects are missing from one replica *after* recovery finished,
etc.).
Ceph检测到PG中对象的一份或多份数据不一致(比如对象大学不一直,或者恢复成功后对象依然没有等)

peering

The placement group is undergoing the peering process
PG正在互联过程中。

阅读全文

本文是Kubernetes DNS-Based Service Discovery的翻译,也就是Kubernetes DNS specification的翻译,目前最新版本号是1.0.1。

0 - 关于此文档

本文档是Kubernetes基于DNS的服务发现的规范说明,尽管在Kubernetes中还有其他方式的服务发现协议和机制,但是DNS仍然是最常见而且是最推荐使用的扩展。实际的DNS服务并不一定由由默认的Kube-DNS提供。 本文档旨在为所有实现之间的通用性提供相应的基准。

阅读全文

在Docker环境中经常会遇到时区相关的问题,所以顺便也看了一下Linux系统下时间相关的一下配置和概念:

硬件时钟

硬件时钟也叫RTC(Real Time Clock)或者CMOS时钟,这个是保存在BIOS中的,仅能保存:年、月、日、时、分、秒这些时间数值,无法保存当前时区以及是否使用夏令时调节。

系统时钟

系统时钟也叫软件时钟,在系统时钟里是有时区等概念的,在Linux内核里,是保存为自 UTC 时间 1970 年1月1日经过的秒数。系统启动时会读取硬件时钟,并根据/etc/adjtime的设置计算当前的时钟。系统启动之后,系统时钟与硬件时钟独立运行,Linux 通过时钟中断计数维护系统时钟。

/etc/localtime

这个文件一般情况下是一个软链接,链接到/usr/share/zoneinfo/目录下的一个对应时区的二进制文件,比如设置Asia/Shanghai的时区,则/etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai,调用date等工具获取时间时会考虑这个配置。
建议并强烈建议将这个文件设置为软链接,很多人会直接拷贝文件,其实是不推荐的。所有的Linux系统都会依赖这个文件。

/etc/timezone

这个文件一般会记录时区的直接文字表示,或者是一个时间偏移(很少见),比如如果设置时区为Asia/Shanghai,则这个文件的内容就会是Asia/Shanghai。这个文件并不是在所有Linux中都存在,比如在我的ArchLinux中就没有这个文件。这个文件一般也仅仅是一个简单的表示。

最后我们基本可以得到下面的结论:

  1. BIOS时间即硬件时间没有时区
  2. Linux在启动时会根据/etc/adjtime的设置和当前的硬件时间计算出当前的UTC时间,并将其和1970 年1月1日的秒差记录在内核中,由时钟中断继续维护。
  3. date等工具获得的时间是根据Linux内核中保存的时间和/etc/localtime的配置计算得来的。

其他相关问题,比如设置时间以及和Windows时间同步等可以参考下面的ArchLinux Wiki链接

参考:

  1. https://wiki.archlinux.org/index.php/Time
  2. https://unix.stackexchange.com/questions/384971/whats-the-difference-between-localtime-and-timezone-files

最近的一个项目需要用到Kubernetes的CronJob,主要用来定时执行一个备份任务,刚开始使用的时候发现没有按照预期的情况运行,所以决定看看CronJob Controller的代码,看看他是怎么实现对应的功能的,正好发现网上也没有其他人写过关于CronJob Controller代码的解析(可能是太简单了不用写吧)。所以也就正好记录一下。

CronJob Controller的代码在kubernetes/pkg/controller/cronjob路径下,主要的逻辑实现在这个目录的cronjob_controller.go,这里分析的是v1.10.2版本的代码,可以直接链接到Github查看。

阅读全文

之前一直听说Designing Data-Intensive Applications (DDIA) 这本书是神书,也决定读一下,顺便做些笔记,也算巩固一下学到的东西吧。

书的第一部分,主要关注于一些基础的知识,第一章的标题Reliable, Scalable, and Maintainable Applications就讲了三个当前应用的最主要特点:可靠性,可扩展性和可维护性。

可靠性

首先是Reliability也就是可靠性,主要包含下面的几个预期:

  • 应用可以按用户所期待的功能正常运行
  • 可以容忍用户犯的错误或者不正确的使用方式
  • 在对应的系统容量下性能能满足正常的使用要求
  • 能拒绝未授权或者滥用的情况

如果能满足上面的需求,可以说一个软件是可靠的,但是并不是所有的东西都能满足预期,当一些意料之外的东西发生了,称之为faults,系统正确应对这些faults的能力则称作fault-tolerant or resilient(即容错能力或者弹性),虽然容错能力很重要,但也不是意味着可以实现一个能容忍任何错误的系统(比如地球爆炸了)。
需要注意的是,faultfailure是不一样的,前者一般指的是系统某个部分没有按照设计正常工作,而后者一般就意味着整个系统都无法正常提供服务了。当然,我们没有办法去降低fault发生的概率,特别是降低到0,能做的,就是当fault发生时,系统不会因为这些faults变成failure状态,这也是一个容错系统的设计目标。
针对一个系统,我们可以人为的提升faults的发生概率,来验证系统的可靠性,比如kill某个进程,或者断开网络等等。一般情况下,比较严重的bug都是因为对错误的处理不完善导致的。
当然尽管我们可以通过设计容忍很多错误,但是预防错误的发生,远远比发生后再去修复来的好,毕竟很多错误是没办法被修复的,比如数据库被黑客入侵,这个操作无法被修复到原始的样子(数据已经泄漏)。
常见的错误主要有三个:

阅读全文

转自The complete guide to Go net/http timeouts

服务端超时

对于http.Server服务端有两个超时可以设置:ReadTimeoutWriteTimeout

1
2
3
4
5
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

各自的作用时间见图:

Server timeout

需要注意的是WriteTimeout被设置了两次,一次是在读取Http头过程中,另一次是在读取Http头结束后。

客户端超时

对于http.Client客户端,相对要复杂一点,一般的初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
c := &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}

这些Timeout各自的作用时间见:

Client timeout

在CNI所以默认提供的Plugin中,bridge应该算是最简单的插件了,针对IPAM Plugin,最简单的应该是host-local,这两个插件也是Kubernetes网络kubenet需要的两个插件。所以这里看一下这两个插件的代码。

所有官方维护的代码,都开源在containernetworking/plugins项目中了。

其中bridge的代码在plugins/main/bridge目录,最重要的是cmdAddcmdDel两个函数,对应CNI SPEC中的ADD和DEL两个主要操作。主要来看一下cmdAdd的实现,精简(删除一些错误处理)后的代码如下:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadNetConf(args.StdinData)

if n.IsDefaultGW {
n.IsGW = true
}

br, brInterface, err := setupBridge(n)
if err != nil {
return err
}

netns, err := ns.GetNS(args.Netns)
defer netns.Close()

hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)

r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
if err != nil {
return err
}

result, err := current.NewResultFromResult(r)

if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}

result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}

gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}

if err := netns.Do(func(_ ns.NetNS) error {
contVeth, err := net.InterfaceByName(args.IfName)

for _, ipc := range result.IPs {
if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
if err := disableIPV6DAD(args.IfName); err != nil {
return err
}
break
}
}

if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}

for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
}); err != nil {
return err
}

if n.IsGW {
var firstV4Addr net.IP
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
for _, gw := range gws.gws {
if gw.IP.To4() != nil && firstV4Addr == nil {
firstV4Addr = gw.IP
}

err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
if err != nil {
return fmt.Errorf("failed to set bridge addr: %v", err)
}
}

if gws.gws != nil {
if err = enableIPForward(gws.family); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err)
}
}
}
}

if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err
}
}
}

br, err = bridgeByName(n.BrName)

brInterface.Mac = br.Attrs().HardwareAddr.String()

result.DNS = n.DNS

return types.PrintResult(result, cniVersion)
}

阅读全文