kube-apiserver代码分析 – API多版本初探

kube-apiserver是k8s中最为核心的组件,对外暴露restful接口,实现对集群中各种资源的增删改查操作。kubelet,kube-*组件都通过apiserver获取自己感兴趣的资源做处理,系统中所有的组件都只负责自己的部分,最终会促使各种资源到达期望的状态。

apiserver中的api资源是由组构成的,叫做ApiGroup,如apps,extensions,每个资源组下面又有不同类型的资源,称为Kind,如Deployment。每个分组下还会有不同的版本,在相同分组的不同版本下面相同的Kind的资源可能会有字段的增删等变更。因此apiserver需要能够正确处理不同版本资源之间的兼容性处理。从废弃策略规则#2中可以了解到,不同版本之间的资源可以互相转换,并且不丢失任何信息,接下来分析apiserver是具体怎么实现的。本文基于k8s的1.8.0版本版本的代码。

首先看下apiserver的启动流程,下面是关键的函数调用链,去掉了不相关代码和条件判断等。

main() cmd/kube-apiserver/apiserver.go:31
  NewAPIServerCommand() cmd/kube-apiserver/app/server.go:99
    Run() cmd/kube-apiserver/app/server.go:151
      CreateServerChain() cmd/kube-apiserver/app/server.go:169
        # 返回的config.ExtraConfig中保存了
        CreateKubeAPIServerConfig() cmd/kube-apiserver/app/server.go:273
          buildGenericConfig() cmd/kube-apiserver/app/server.go:417
            # 加载当前版本默认api资源配置
            genericConfig.MergedResourceConfig = master.DefaultAPIResourceConfigSource() cmd/kube-apiserver/app/server.go:431
            # 将参数配置中的--runtime-config应用到默认api资源配置
            s.APIEnablement.ApplyTo(genericConfig, master.DefaultAPIResourceConfigSource(), legacyscheme.Scheme) cmd/kube-apiserver/app/server.go:449
            # 设置存储端后端对应的api操作接口
            storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
            # 将apiserver和storage的配置返回了,就是说apiserver和storage都是使用的用户自定义的配置对默认配置进行覆盖后的配置
            # 返回的 genericConfig.MergedResourceConfig保存了api启用配置,类型为*storage.ResourceConfig
            # 返回的storageFactory.APIResourceConfig也保存了api启用配置,类型也为*storage.ResourceConfig
        CreateKubeAPIServer() cmd/kube-apiserver/app/server.go:191
          kubeAPIServerConfig.Complete().New() cmd/kube-apiserver/app/server.go:219
            # 启用legacy api(/api/v1)
            m.InstallLegacyAPI() pkg/master/master.go:405
            # 启用api(/apis/{groupn})
            m.InstallAPIs() 
              m.GenericAPIServer.InstallAPIGroups() pkg/master/master.go:553
``

其中m.InstallLegacyAPI()是安装旧版接口,及/api/v1接口路径下的接口。我们主要看m.InstallAPIS这部分,这部分安装的接口的都在/apis/{group}路径下。

现在深入到InstallAPIGroups()这个函数中以及看后面的详细调用链。

// Exposes given api groups in the API.
func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error {
    // 安装api资源
    s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels)

    // type APIServerHandler struct {
    //    FullHandlerChain   http.Handler
    //    GoRestfulContainer *restful.Container
    //    NonGoRestfulMux    *mux.PathRecorderMux
    //    Director           http.Handler
    // }
    // 其中restful.Container为go的restful框架中server实例
    //
    // type APIGroup struct {
    //    Name                       string
    //    Versions                   []GroupVersionForDiscovery
    //    PreferredVersion           GroupVersionForDiscovery
    //    ServerAddressByClientCIDRs []ServerAddressByClientCIDR
    // }
    //
    // type APIGroupHandler struct {
    //     serializer runtime.NegotiatedSerializer
    //     group      v1.APIGroup
    // }
    // NewApiGroupHandler返回上述结构体,serializer为序列化和反序列化器
    //
    // type NegotiatedSerializer interface {
    //     SupportedMediaTypes() []SerializerInfo
    //     EncoderForVersion(serializer Encoder, gv GroupVersioner) Encoder
    //     DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder
    // }
    // 序列化器主要用来处理http请求,可将runtime.Oject对象编码为指定版本或从指定版本得到runtime.Object
    // schema代表了k8s资源对象的内部表示形式
    //
    // APIGroupHandler实现了gorestful框架的WebService接口
    s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
}

这个函数主要将该group安装到 /apis/<groupName>路径下,访问这个路径能够获取所有支持的版本和描述等信息,不必重点看这块。

func (s *APIGroupHandler) WebService() *restful.WebService {
    mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
    ws := new(restful.WebService)
    ws.Path(APIGroupPrefix + "/" + s.group.Name)
    ws.Doc("get information of a group")
    ws.Route(ws.GET("/").To(s.handle).
        Doc("get information of a group").
        Operation("getAPIGroup").
        Produces(mediaTypes...).
        Consumes(mediaTypes...).
        Writes(metav1.APIGroup{}))
    return ws
}

// handle returns a handler which will return the api.GroupAndVersion of the group.
func (s *APIGroupHandler) handle(req *restful.Request, resp *restful.Response) {
    s.ServeHTTP(resp.ResponseWriter, req.Request)
}

func (s *APIGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, &s.group)
}

下面是安装各种api资源的地方,将各group下的所有支持的不同版本注册到REST服务当中。

// installAPIResources is a private method for installing the REST storage backing each api groupversionresource
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels openapiproto.Models) error {
    for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
        if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
            klog.Warningf("Skipping API %v because it has no resources.", groupVersion)
            continue
        }

        apiGroupVersion := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
        if apiGroupInfo.OptionsExternalVersion != nil {
            apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
        }
        apiGroupVersion.OpenAPIModels = openAPIModels
        apiGroupVersion.MaxRequestBodyBytes = s.maxRequestBodyBytes

        if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
            return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
        }
    }

    return nil
}

func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
    prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
    installer := &APIInstaller{
        group:             g,
        prefix:            prefix,
        minRequestTimeout: g.MinRequestTimeout,
    }

    apiResources, ws, registrationErrors := installer.Install()
    versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, staticLister{apiResources})
    versionDiscoveryHandler.AddToWebService(ws)
    container.Add(ws)
    return utilerrors.NewAggregate(registrationErrors)
}


func (a *APIInstaller) Install() ([]metav1.APIResource, *restful.WebService, []error) {
    var apiResources []metav1.APIResource
    var errors []error
    ws := a.newWebService()

    // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
    paths := make([]string, len(a.group.Storage))
    var i int = 0
    for path := range a.group.Storage {
        paths[i] = path
        i++
    }
    sort.Strings(paths)
    for _, path := range paths {
        // 将storage中定义的路径映射到api接口的路径当中,并传递对应的rest.Storage到handler函数当中。
        // 在registerResourceHandlers当中会对这些路径注册不同的增删改查的操作
        apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
        if err != nil {
            errors = append(errors, fmt.Errorf("error in registering resource: %s, %v", path, err))
        }
        if apiResource != nil {
            apiResources = append(apiResources, *apiResource)
        }
    }
    return apiResources, ws, errors
}

// 这个函数比较长,拿出最关键的关于POST和GET方法的路由注册
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
        // 这个地方是针对创建资源的handler注册,将资源路径->handler注册到restful的WebService中。handler的是由restfulCreateResource创建的,其中的creater参数是rest.Creater接口,上面提到的rest.Storage里实现了对应的处理方法,如pkg/registry/apps/deployment/storage/storage.go中的DeploymentStorage,改接口中内嵌了REST结构,并且REST中内嵌了vendor/k8s.io/apiserver/pkg/registry/generic/registry/store.go中的Store结构,改结构实现了pkg/api/rest.StandardStorage中的标准接口。
        case "POST": // Create a resource.
            var handler restful.RouteFunction
            if isNamedCreater {
                handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
            } else {
                handler = restfulCreateResource(creater, reqScope, admit)
            }
            handler = metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, handler)
            article := GetArticleForNoun(kind, " ")
            doc := "create" + article + kind
            if isSubresource {
                doc = "create " + subresource + " of" + article + kind
            }
            route := ws.POST(action.Path).To(handler).
                Doc(doc).
                Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
                Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
                Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
                Returns(http.StatusOK, "OK", producedObject).
                // TODO: in some cases, the API may return a v1.Status instead of the versioned object
                // but currently go-restful can't handle multiple different objects being returned.
                Returns(http.StatusCreated, "Created", producedObject).
                Returns(http.StatusAccepted, "Accepted", producedObject).
                Reads(defaultVersionedObject).
                Writes(producedObject)
            if err := AddObjectParams(ws, route, versionedCreateOptions); err != nil {
                return nil, err
            }
            addParams(route, action.Params)
            routes = append(routes, route)
}

func restfulCreateResource(r rest.Creater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
    return func(req *restful.Request, res *restful.Response) {
        handlers.CreateResource(r, &scope, admit)(res.ResponseWriter, req.Request)
    }
}

func CreateResource(r rest.Creater, scope *RequestScope, admission admission.Interface) http.HandlerFunc {
    return createHandler(&namedCreaterAdapter{r}, scope, admission, false)
}

func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
    // 这行就是把请求的内容按照请求的版本解码出来,解码之后是一个runtime.Object对象
    obj, gvk, err := decoder.Decode(body, &defaultGVK, original)
    return func(w http.ResponseWriter, req *http.Request) {
        requestFunc := func() (runtime.Object, error) {
            // 这里的r.Create就调用到了Storage的创建接口了
            return r.Create(
                ctx,
                name,
                obj,
                rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
                options,
            )
        }
        result, err := requestFunc()
    }
}

接下来就是使用etcd的实现将runtime.Object写到etcd中,关于etcd的存储初始化,没有具体跟踪。在写入etcd的时候数据也是经过反序列化的,apiserver中提供了protobuf和json两种序列化方法,目前看默认是使用protobuf对etcd中的数据进行编解码(从etcd获取二进制格式数据饭钱是代码中protobuf codec定义的魔术字段\x6b\x38\x73\x00)。

k8s中的资源对象从api的http请求中通过反序列化为runtime.Ojbect对象,在将这个对象序列化到etcd中,存到etcd中时会优先使用代码中定义的优先版本进行存储。如下面代码中把SetVersionPriority指定的优先级对换一下位置再看etcd中存储的数据及为v1beta2版本(测试用Deployment)。

+++ b/pkg/apis/apps/install/install.go
@@ -38,5 +38,5 @@ func Install(scheme *runtime.Scheme) {
        utilruntime.Must(v1beta1.AddToScheme(scheme))
        utilruntime.Must(v1beta2.AddToScheme(scheme))
        utilruntime.Must(v1.AddToScheme(scheme))
-       utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
+       utilruntime.Must(scheme.SetVersionPriority(v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1.SchemeGroupVersion))
 }

另外apiserver在从etcd读取数据的时候是通过Decoder来解码数据的,上面说到实际数据前面有4个字节的protobuf的魔术数字,因此apiserver可以自动这个数字自动探测应该使用哪个解码器。数据解码之后也是runtime.Object对象,然后再序列化成客户端指定的json/yaml等格式。关于资源对象的编解码部分都在k8s.io/apimachinery这个包下,后续再详细对这个包进行分析介绍。

linux配置多级服务器登录和隧道映射

通常,办公环境的电脑无法直接连接到开发测试服务器,往往需要进行多次ssh跳转。这时可通过配置ssh支持自动跳转登录功能。假设有2台服务器A和B在我们的开发测试环境中,本地我们只能连接到A服务器,而A服务器可以连接到B服务器。编辑~/.ssh/config文件,输入下列内容:

Host serverA
    User root
    HostName 10.1.2.3
    IdentityFile /home/myusername/.ssh/id_rsa
    Port 22

Host serverB
    User root
    HostName 192.168.1.3
    IdentityFile /home/myusername/.ssh/id_rsa
    port 22
    ProxyJump mypub

此时,在本地即可通过ssh serverB直接连接到服务器B的ssh服务。如果要使用免密方式进行登录。则需要执行ssh-copy-id root@serverA和ssh-copy-id root@serverB进行配置免密。

在进行了这样的配置之后,我们也可以更加方便的建立隧道来使用了。比如在服务器上有一个8080的http服务,想要在本地浏览器里访问。那么,可以执行下列命令建立隧道:

ssh -Nf -Llocalhost:8080:192.168.1.3:8080 serverB

此命令会将serverB上的8080端口映射到本地的8080端口上,在浏览器中我们只需要输入http://localhost:8080可以访问到serverB上的web服务。

同样,也可以通过-R选项将本地端口映射到serverB的某个端口上,比如在微信公众号开发测试的时候,可以将配置在公众平台的服务器地址上的服务映射到本地。

为了避免网络连接中断导致隧道断开连接,可以使用systemd来保活,隧道进程退出后自动将其拉起:

# cat /lib/systemd/system//my-http-proxy.service
[Unit]
Describe=Kmr nginx agent
After=network.target
[Service]
LimitNOFILE=10000
Type=simple
User=root
Group=root
ExecStart=/usr/bin/ssh -oExitOnForwardFailure=yes -oPubkeyAuthentication=yes -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oServerAliveInterval=5 -oServerAliveCountMax=3 -Llocalhost:8080:localhost:8080 -N root@serverB
Restart = always
RestartSec = 1s
StartLimitInterval = 0

[Install]
WantedBy=network.target

执行systemctl daemon-reload和systemctl start my-http-proxy来启动服务。

linux下tcp keepalive相关参数调整测试

首先说明下面三个和keepalive相关的内核参数以及默认的值

# sysctl -a | grep keepalive
# 在会后一次发送数据包后多久向对方发起探测
net.ipv4.tcp_keepalive_time = 7200
# 在没有收到对方确认时,会按照这个时间间隔再次探测
net.ipv4.tcp_keepalive_intvl = 75
# 在没有收到对方确认时,进行探测的次数
net.ipv4.tcp_keepalive_probes = 9

下面通过在本地环境上测试这些参数,首先将本地的默认keepalive参数进行修改

# sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 20

下载并编译带keepalive功能支持的netcat命令行工具

git clone https://github.com/cyberelf/netcat-keepalive.git
cd netcat-keepalive/
make linux

运行tcpdump进行抓包

tcpdump -iany port 18888

启动服务端监听

./nckl-linux -v4K -l 18888

使用nc去连接

nc -v -p55666 localhost 18888

可以看到抓包内容如下

root@debian:/home/blue# tcpdump -iany port 18888
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
15:42:49.840078 IP localhost.55666 > localhost.18888: Flags [S], seq 4147250120, win 65495, options [mss 65495,sackOK,TS val 3777822786 ecr 0,nop,wscale 7], length 0
15:42:49.840104 IP localhost.18888 > localhost.55666: Flags [S.], seq 2015925109, ack 4147250121, win 65483, options [mss 65495,sackOK,TS val 3777822786 ecr 3777822786,nop,wscale 7], length 0
15:42:49.840124 IP localhost.55666 > localhost.18888: Flags [.], ack 1, win 512, options [nop,nop,TS val 3777822786 ecr 3777822786], length 0
# 这里三次握手结束并且在客户端发送了一个字符
15:42:50.991399 IP localhost.55666 > localhost.18888: Flags [P.], seq 1:4, ack 1, win 512, options [nop,nop,TS val 3777823937 ecr 3777822786], length 3
15:42:50.991421 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777823937 ecr 3777823937], length 0
# 从这里开始下面是每隔20秒钟进行一次探测
15:43:11.005311 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777843951 ecr 3777823937], length 0
15:43:11.005343 IP localhost.55666 > localhost.18888: Flags [.], ack 1, win 512, options [nop,nop,TS val 3777843951 ecr 3777823937], length 0
15:43:31.101311 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777864047 ecr 3777843951], length 0
15:43:31.101346 IP localhost.55666 > localhost.18888: Flags [.], ack 1, win 512, options [nop,nop,TS val 3777864047 ecr 3777823937], length 0
15:43:51.325306 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777884271 ecr 3777864047], length 0
15:43:51.325328 IP localhost.55666 > localhost.18888: Flags [.], ack 1, win 512, options [nop,nop,TS val 3777884271 ecr 3777823937], length 0
# 在这里执行了一个iptables -A INPUT -p tcp --dport 55666 -j DROP命令,可以看到在没有收到对方应答的情况下会每隔5秒进行一次探测,连续9次没有收到应答,操作系统重置了这个连接
15:44:11.549314 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777904495 ecr 3777884271], length 0
15:44:16.669304 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777909615 ecr 3777884271], length 0
15:44:21.789312 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777914735 ecr 3777884271], length 0
15:44:26.909310 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777919855 ecr 3777884271], length 0
15:44:32.029305 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777924975 ecr 3777884271], length 0
15:44:37.149314 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777930095 ecr 3777884271], length 0
15:44:42.269309 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777935215 ecr 3777884271], length 0
15:44:47.389304 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777940335 ecr 3777884271], length 0
15:44:52.509319 IP localhost.18888 > localhost.55666: Flags [.], ack 4, win 512, options [nop,nop,TS val 3777945455 ecr 3777884271], length 0
15:44:57.629320 IP localhost.18888 > localhost.55666: Flags [R.], seq 1, ack 4, win 512, options [nop,nop,TS val 3777950575 ecr 3777884271], length 0

记一次k8s集群pod一直terminating问题的排查

现象描述

pod一直处于terminating状态,或者很久才能删除,内核日志中持续打印unregister_netdevice: waiting for XXX to become free. Usage count = 1。

故障诊断

经过定位和排查,定位到是内核的一个bug导致网络设备无法删除。具体参考https://github.com/torvalds/linux/commit/ee60ad219f5c7c4fb2f047f88037770063ef785f

另外在github的k8s的issues里也有该bug的相关讨论。有人给出了付现这个问题的方式,以及验证上面提到的修复方法是否有效。下面是按照他给出的方案做的复现和验证。具体可参考https://github.com/moby/moby/issues/5618#issuecomment-549333485

问题排查

从kubelet内核日志来看是在删除pod的网卡设备时因为内核的引用计数bug,导致无法删除。后续对网卡信息的查询和再次删除操作应该也会导致超时失败(根据日志推断,暂时还未在代码中找到对应调用,线上环境也没法重启调整日志级别和调试)。

首先需要看一个概念:PLEG。

PLEG (pod lifecycle event generator) 是 kubelet 中一个非常重要的模块,它主要完成以下几个目标:

  • 从 runtime 中获取 pod 当前状态,产生 pod lifecycle events
  • 从 runtime 中获取 pod 当前状态,更新 kubelet pod cache

接下来分析一下造成问题的原因应该是k8s的PLEG在同步pod信息时,可能要查询网卡详情(ip地址),由于内核bug导致超时,致使syncLoop中每执行一次遍历的时间过长(4分钟左右),因此新建pod和删除pod的时候,node上的信息和server上的信息更新不及时。用busybox测试创建和删除时,通过docker ps可以看到响应容器很快就可以启动或删除掉。

从图中可以看到该日志:Calico CNI deleting device in netns /proc/16814/ns/net这条。这是在pod执行删除是产生的。在正常情况下后面会有删除完成的日志信息,如下图:

但上面的日志里的无此信息,并且10s后打印了unregister_netdevice xxx的日志。这里是触发了内核bug。通过ps aux | grep calico也可以看到在对应时间有一个calico进程启动去执行操作,目前这个进程还在(10.209.33.105),这里估计k8s也有bug,没有wait pid,导致calico成为僵尸进程。

下图是kubelet日志。其中的PLEG is not healthy日志也是在对应的时间点出现。

问题本地复现

要在本地复现这个问题,首先需要给内核打补丁来协助复现。

diff --git a/net/ipv4/route.c b/net/ipv4/route.c
index a0163c5..6b9e7ee 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -133,6 +133,8 @@

 static int ip_min_valid_pmtu __read_mostly	= IPV4_MIN_MTU;

+static int ref_leak_test;
+
/*
  *	Interface to generic destination cache.
  */
@@ -1599,6 +1601,9 @@ static void ip_del_fnhe(struct fib_nh *nh, __be32 daddr)
 	fnhe = rcu_dereference_protected(*fnhe_p, lockdep_is_held(&fnhe_lock));
	while (fnhe) {
 		if (fnhe->fnhe_daddr == daddr) {
+			if (ref_leak_test)
+				pr_info("XXX pid: %d, %s: fib_nh:%p, fnhe:%p, daddr:%x\n",
+					current->pid,  __func__, nh, fnhe, daddr);
 			rcu_assign_pointer(*fnhe_p, rcu_dereference_protected(
 				fnhe->fnhe_next, lockdep_is_held(&fnhe_lock)));
 			fnhe_flush_routes(fnhe);
@@ -2145,10 +2150,14 @@ static struct rtable *__mkroute_output(const struct fib_result *res,

		fnhe = find_exception(nh, fl4->daddr);
 		if (fnhe) {
+			if (ref_leak_test)
+				pr_info("XXX pid: %d, found fnhe :%p\n", current->pid, fnhe);
 			prth = &fnhe->fnhe_rth_output;
 			rth = rcu_dereference(*prth);
 			if (rth && rth->dst.expires &&
`			    time_after(jiffies, rth->dst.expires)) {
+				if (ref_leak_test)
+					pr_info("eXX pid: %d, del fnhe :%p\n", current->pid, fnhe);
				ip_del_fnhe(nh, fl4->daddr);
 				fnhe = NULL;
 			} else {
@@ -2204,6 +2213,14 @@ static struct rtable *__mkroute_output(const struct fib_result *res,
 #endif
 	}

+	if (fnhe && ref_leak_test) {
+		unsigned long  time_out;
+
+		time_out = jiffies + ref_leak_test;
+		while (time_before(jiffies, time_out))
+			cpu_relax();
+		pr_info("XXX pid: %d, reuse fnhe :%p\n", current->pid, fnhe);
+	}
 	rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0);
 	if (lwtunnel_output_redirect(rth->dst.lwtstate))
 		rth->dst.output = lwtunnel_output;
@@ -2733,6 +2750,13 @@ static int ipv4_sysctl_rtcache_flush(struct ctl_table *__ctl, int write,
		.proc_handler	= proc_dointvec,
	},
 	{
+		.procname	= "ref_leak_test",
+		.data		= &ref_leak_test,
+		.maxlen		= sizeof(int),
+		.mode		= 0644,
+		.proc_handler	= proc_dointvec,
+	},
+	{
		.procname	= "max_size",
		.data		= &ip_rt_max_size,
 		.maxlen		= sizeof(int),

编译内核的详细步骤参考:https://wiki.centos.org/zh/HowTos/Custom_Kernel

添加用户useradd kernel-build

下载内核源码kernel-3.10.0-693.el7.src.rpm,拷贝到/home/kernel-build,并切换到kernel-build用户。

执行rpm -i kernel-3.10.0-693.el7.src.rpm | grep -v exist解压源码包。

进入cd /home/kernel-build/rpmbuild目录。

修改rpm打包文件vim SPECS/kernel.spec,添加patch说明。

ApplyOptionalPatch netdev-leak.patch

编辑并生成patch文件,保存到SOURCES/netdev-leak.patch,其内容为:

--- a/net/ipv4/route.c	2017-07-07 07:37:46.000000000 +0800
+++ b/net/ipv4/route.c	2020-05-06 17:33:19.746187091 +0800
@@ -129,6 +129,7 @@
 static int ip_rt_min_advmss __read_mostly	= 256;
 
 static int ip_rt_gc_timeout __read_mostly	= RT_GC_TIMEOUT;
+static int ref_leak_test;
 /*
  *	Interface to generic destination cache.
  */
@@ -1560,8 +1561,15 @@
 	fnhe = rcu_dereference_protected(*fnhe_p, lockdep_is_held(&fnhe_lock));
 	while (fnhe) {
 		if (fnhe->fnhe_daddr == daddr) {
+			if (ref_leak_test)
+				pr_info("XXX pid: %d, %s: fib_nh:%p, fnhe:%p, daddr:%x\n",
+					current->pid,  __func__, nh, fnhe, daddr);
 			rcu_assign_pointer(*fnhe_p, rcu_dereference_protected(
 				fnhe->fnhe_next, lockdep_is_held(&fnhe_lock)));
+			/* set fnhe_daddr to 0 to ensure it won't bind with
+  			 * new dsts in rt_bind_exception().
+ 			 */
+			// fnhe->fnhe_daddr = 0; 这行是修复代码,复现问题的时候不需要,注释掉
 			fnhe_flush_routes(fnhe);
 			kfree_rcu(fnhe, rcu);
 			break;
@@ -2054,10 +2062,14 @@
 
 		fnhe = find_exception(nh, fl4->daddr);
 		if (fnhe) {
+			if (ref_leak_test)
+				pr_info("XXX pid: %d, found fnhe :%p\n", current->pid, fnhe);
 			prth = &fnhe->fnhe_rth_output;
 			rth = rcu_dereference(*prth);
 			if (rth && rth->dst.expires &&
 			    time_after(jiffies, rth->dst.expires)) {
+				if (ref_leak_test)
+					pr_info("eXX pid: %d, del fnhe :%p\n", current->pid, fnhe);
 				ip_del_fnhe(nh, fl4->daddr);
 				fnhe = NULL;
 			} else {
@@ -2122,6 +2134,14 @@
 #endif
 	}
 
+	if (fnhe && ref_leak_test) {
+		unsigned long  time_out;
+
+		time_out = jiffies + ref_leak_test;
+		while (time_before(jiffies, time_out))
+			cpu_relax();
+		pr_info("XXX pid: %d, reuse fnhe :%p\n", current->pid, fnhe);
+	}
 	rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0);
 	if (lwtunnel_output_redirect(rth->dst.lwtstate))
 		rth->dst.output = lwtunnel_output;
@@ -2661,6 +2681,13 @@
 		.maxlen		= sizeof(int),
 		.mode		= 0644,
 		.proc_handler	= proc_dointvec,
+	},
+	{
+		.procname	= "ref_leak_test",
+		.data		= &ref_leak_test,
+		.maxlen		= sizeof(int),
+		.mode		= 0644,
+		.proc_handler	= proc_dointvec,
 	},
 	{
 		.procname	= "max_size",

执行rpmbuild -bb –target=`uname -m` SPECS/kernel.spec 2> build-err.log | tee build-out.log

安装新内:

yum localinstall RPMS/x86_64/kernel-3.10.0-693.el7.centos.x86_64.rpm

编辑ref_leak_test_begin.sh

#!/bin/bash

# constructing a basic network with netns
# client <-->gateway <--> server
ip netns add svr
ip netns add gw
ip netns add cli

ip netns exec gw sysctl net.ipv4.ip_forward=1

ip link add svr-veth type veth peer name svrgw-veth
ip link add cli-veth type veth peer name cligw-veth

ip link set svr-veth netns svr
ip link set svrgw-veth netns gw
ip link set cligw-veth netns gw
ip link set cli-veth netns cli

ip netns exec svr ifconfig svr-veth 192.168.123.1
ip netns exec gw ifconfig svrgw-veth 192.168.123.254
ip netns exec gw ifconfig cligw-veth 10.0.123.254
ip netns exec cli ifconfig cli-veth 10.0.123.1

ip netns exec cli route add default gw 10.0.123.254
ip netns exec svr route add default gw 192.168.123.254

# constructing concurrently accessed scenes with nerperf
nohup ip netns exec svr  netserver -L 192.168.123.1

nohup ip netns exec cli  netperf -H 192.168.123.1 -l 300 &
nohup ip netns exec cli  netperf -H 192.168.123.1 -l 300 &
nohup ip netns exec cli  netperf -H 192.168.123.1 -l 300 &
nohup ip netns exec cli  netperf -H 192.168.123.1 -l 300 &

# Add delay
echo 3000 > /proc/sys/net/ipv4/route/ref_leak_test

# making PMTU discovery exception routes
echo 1 >  /proc/sys/net/ipv4/route/mtu_expires
for((i=1;i<=60;i++));
do
  for j in 1400  1300 1100 1000
  do
	echo "set mtu to "$j;
	ip netns exec svr ifconfig  svr-veth  mtu $j;
	ip netns exec cli ifconfig  cli-veth  mtu $j;
	ip netns exec gw ifconfig svrgw-veth  mtu $j;
	ip netns exec gw ifconfig cligw-veth  mtu $j;
	sleep 2;
  done
done

编辑ref_leak_test_end.sh

#!/bin/bash

echo 0 > /proc/sys/net/ipv4/route/ref_leak_test

pkill netserver
pkill netperf

ip netns exec cli ifconfig cli-veth down
ip netns exec gw ifconfig svrgw-veth down
ip netns exec gw ifconfig cligw-veth down
ip netns exec svr ifconfig svr-veth down

ip netns del svr
ip netns del gw
ip netns del cli

执行测试,首先执行bash ref_leak_test_begin.sh,等待数秒至一分钟时间。Ctrl + C结束,执行bash ref_leak_test_end.sh。大概在10秒钟之内会打印下列信息:

[root@VM_1_72_centos ~]# bash ref_leak_test_begin.sh 
net.ipv4.ip_forward = 1
nohup: 忽略输入并把输出追加到"nohup.out"
nohup: 把输出追加到"nohup.out"
nohup: 把输出追加到"nohup.out"
set mtu to 1400
nohup: 把输出追加到"nohup.out"
nohup: 把输出追加到"nohup.out"
set mtu to 1300
set mtu to 1100
set mtu to 1000
set mtu to 1400
set mtu to 1300
^C^C
[root@VM_1_72_centos ~]# bash ref_leak_test_end.sh 
[root@VM_1_72_centos ~]# ip netns list
Message from syslogd@VM_1_72_centos at May  6 17:43:49 ...
 kernel:unregister_netdevice: waiting for cli-veth to become free. Usage count = 1

[root@VM_1_72_centos ~]# ip netns list
[root@VM_1_72_centos ~]# 
Message from syslogd@VM_1_72_centos at May  6 17:43:59 ...
 kernel:unregister_netdevice: waiting for cli-veth to become free. Usage count = 1

Message from syslogd@VM_1_72_centos at May  6 17:44:09 ...
 kernel:unregister_netdevice: waiting for cli-veth to become free. Usage count = 1

Message from syslogd@VM_1_72_centos at May  6 17:44:19 ...
 kernel:unregister_netdevice: waiting for cli-veth to become free. Usage count = 1

Message from syslogd@VM_1_72_centos at May  6 17:44:29 ...
 kernel:unregister_netdevice: waiting for cli-veth to become free. Usage count = 1

现在可以复现出unregister_netdevice: waiting for XXX to become free. Usage count = 1的问题。

修复和验证

问题修复的patch是修改内核代码net/ipv4/route.c中的下列内容:

@@ -1303,6 +1303,10 @@ static void ip_del_fnhe(struct fib_nh *nh, __be32 daddr)
		if (fnhe->fnhe_daddr == daddr) {
			rcu_assign_pointer(*fnhe_p, rcu_dereference_protected(
				fnhe->fnhe_next, lockdep_is_held(&fnhe_lock)));
			/* set fnhe_daddr to 0 to ensure it won't bind with
			 * new dsts in rt_bind_exception().
			 */
			fnhe->fnhe_daddr = 0;
			fnhe_flush_routes(fnhe);
			kfree_rcu(fnhe, rcu);
			break;

将这段补丁代码打入内核中,可参考netdev-leak.patch中,重新编译、安装内核。

再次执行上面的bash ref_leak_test_begin.sh和bash ref_leak_test_end.sh发现不会在打印unregister_netdevice: waiting for XXX to become free. Usage count = 1的日志。说明这段代码起作用了。

除了等待eth0这个问题,还有一个等待lo的类似问题,也有可能会出现。新版内核得到修复(v4.15),不过目前还没遇到这个问题。

另外对于pod terminating问题我们的内部环境上还没复现,个人觉得可参考ref_leak_test_begin.sh中的做法,在一个pod内向另一个pod发起大量的tcp连接请求进行测试。

参考

轮指和修指甲小计

轮指

拇指要放松才能弹均匀,四个手指头下意识去独立控制,ima拨弦时要快速发力。90左右慢速练习时要把声音弹足够大和清晰。

指甲

由于m型指甲,斜坡不用太长,斜坡太长会有指甲摩擦琴弦的声音,最高坡点在手指中心偏向小指方向一点点的位置。指甲不用太长,从正面看相对于指肚多出1至2毫米即可。拨弦方向在标准姿势稍垂直于琴弦一点点,可以有效利用m型指甲中间凹点带动下压琴弦。合理的指甲形状可以通过拨弦时的阻力确定,阻力太小太顺滑发不出圆润的声音,因为无法有效的下压琴弦,要调整到一个阻力合适的形状。

古典吉他学习第十七十八节课总结

课上主要看练习效果。注意练习的时候声音要大,要放开。注意保留指,旋律音不要用保留指,是多少时值就响多久。伴奏是和弦的话可以。要区分出旋律和伴奏,也要注意多个声部以及旋律在声部之间的变换。还需在深入。

对于节奏的把握需要继续练,尤其附点音符。

练习

1. 和声音程

2. 《铜壶的四周》

八分音符 = 75 90 105

3. 《古典舞蹈》

八分音符 = 70 80 90

4. 《土拨鼠》

八分音符 = 70 80 90

5. 视奏《小精灵》、《多年以后》、《冒险记》

古典吉他学习第十三十四节课总结

课上主要看练习效果。

练习

1. 卡鲁里

1~9型 1:2 85   100  115, 1:3 60 70 80

10,11型 1:1 110 120 130

2. 和声音程

变型 1:1 90 105 120

标准 2:1 60 70 80

1-9型 1:1 140 155 170

3. 《我怎能离开你》 低音版

四分音符 110 120 130

4. 《我怎能离开你》 高音版

四分音符 60 70 80

5. 《视奏《古典舞蹈》》

古典吉他学习第十一十二节课总结

本节课的问题是演奏没有区分强弱因,强弱弱的节奏谈成了一样的,低音是主旋律,高音是伴奏。注意低音的强度,回来发现和拇指指甲修剪的形状有关,重新调整之后,稍微再用力些按弦,确实低音的音量更加饱满。同时锻炼手指灵活度。

练习

1. 和声音程

变型 1:1 90   105  120

正常 2:1 60 70 80

2. 卡路里

10型,11型 1:1 70 85 100

12型 2:1 70 85 100

1-9型 1:1 140 155 170

3. 我怎能离开你 P指

四分音符 = 60 70 80

4. 视奏高音版 我怎能离开你

 

保持打拨弦和开指练习,加入右手反弹,左右手交替练习。

 

同时记录下回来之后找到的最好发音拨弦和指甲形状

古典吉他学习第九十节课总结

老师说发音很饱满,拨弦还不够稳定,需要再进一步加强稳定性练习,只之前的速度上继续提升一些,巩固一下发音,不可过快,打好基本功很重要。

练习

1.和声音程

变型 1:1 55 70 85

Pi Pm pi pm pi mp

Pm Pa pm pa pm pa

正常 2:1 60 75 90

2. 卡鲁里

1-9型 1:1 110 125 140 (155)

视奏10-12型

3. 我怎能离开你

八分音符 = 80 95 110 (125)

4. 开指

1:1 60 90 100

5. 打拨弦

1:1 90 100 110