标签 github 下的文章

Kubernetes节点资源耗尽状态的处理

今天上午一到工位,就收到来自同事的“投诉”:私有云上的Kubernetes cluster中的一个node似乎不工作了,因为专门部署于那个节点上的应用挂掉了,并且长时间没有恢复。这个公司私有云上Kubernetes集群是v1.7.5版本,部署于双节假期之前。最近感觉K8s开发明显提速,连续发布版本,截至发稿时,最新发布的版本为v1.8.1了。这个集群一直运行相对稳定,今天这个异常到底是怎么一回事呢?于是打开terminal,开始了问题的调查。

一、问题现象

我们这个小集群一共有三个Kubernetes Node。首先,我查看集群中的所有Pods状态,发现node1和node2上的Pods均正常(running状态),但位于node3上的三个Pods均为“Pending”状态,这三个pod是weave-net-rh6r4、kube-proxy-v4d1p以及portal-3613605798-txq4l,其中portal-3613605798-txq4l是我们的应用Pod。K8s自身的组件kube-proxy都异常了,显然node3节点出问题了。如果你此刻去尝试查看(kubectl describe) 这几个pod的状态,多半你会失败,因为Pod在频繁重启,1-2s钟新创建的Pod就会被kill掉,导致你无法查看其状态。

我直接查看一下node3的状态,果不其然,我得到了一些Warning events:

# kubectl describe ubuntu-k8s-3
... ...
Events:
  FirstSeen    LastSeen    Count    From            SubObjectPath    Type        Reason            Message
  ---------    --------    -----    ----            -------------    --------    ------            -------
  51m        51m        1    kubelet, ubuntu-k8s-3            Normal        NodeNotSchedulable    Node ubuntu-k8s-3 status is now: NodeNotSchedulable
  9d        51m        49428    kubelet, ubuntu-k8s-3            Warning        EvictionThresholdMet    Attempting to reclaim nodefs
  5m        5m        1    kubelet, ubuntu-k8s-3            Normal        Starting        Starting kubelet.
  5m        5m        2    kubelet, ubuntu-k8s-3            Normal        NodeHasSufficientDisk    Node ubuntu-k8s-3 status is now: NodeHasSufficientDisk
  5m        5m        2    kubelet, ubuntu-k8s-3            Normal        NodeHasSufficientMemory    Node ubuntu-k8s-3 status is now: NodeHasSufficientMemory
  5m        5m        2    kubelet, ubuntu-k8s-3            Normal        NodeHasNoDiskPressure    Node ubuntu-k8s-3 status is now: NodeHasNoDiskPressure
  5m        5m        1    kubelet, ubuntu-k8s-3            Normal        NodeAllocatableEnforced    Updated Node Allocatable limit across pods
  5m        5m        1    kubelet, ubuntu-k8s-3            Normal        NodeHasDiskPressure    Node ubuntu-k8s-3 status is now: NodeHasDiskPressure
  5m        14s        23    kubelet, ubuntu-k8s-3            Warning        EvictionThresholdMet    Attempting to reclaim nodefs

两点有价值的内容:
1、Node ubuntu-k8s-3 status is now: NodeHasDiskPressure
2、Warning: “EvictionThresholdMet Attempting to reclaim nodefs”

从以上内容大致可以判断出node3处于磁盘空间不足的状态下,并且该node上的kubelet daemon判断达到了Eviction阀值,试图回收磁盘空间(通过某种杀Pod的方式,I Guess)。

既然提到了Kubelet,我们再来看看这一后台service的log:

# journalctl  -u kubelet -f
10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: W1016 09:50:55.056703   17144 eviction_manager.go:331] eviction manager: attempting to reclaim nodefs
10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: I1016 09:50:55.057322   17144 eviction_manager.go:345] eviction manager: must evict pod(s) to reclaim nodefs
10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: E1016 09:50:55.058307   17144 eviction_manager.go:356] eviction manager: eviction thresholds have been met, but no pods are active to evict
... ...
10月 16 09:54:14 ubuntu-k8s-3 kubelet[12844]: W1016 09:54:14.823152   12844 eviction_manager.go:142] Failed to admit pod weave-net-3svfg_kube-system(e5a5d474-b214-11e7-a98b-0650cc001a5b) - node has conditions: [DiskPressure]
10月 16 09:54:14 ubuntu-k8s-3 kubelet[12844]: W1016 09:54:14.824246   12844 eviction_manager.go:142] Failed to admit pod kube-proxy-d9lk0_kube-system(e5ff8fde-b214-11e7-a98b-0650cc001a5b) - node has conditions: [DiskPressure]

kubelet日志也印证了上面的判断:node3因为磁盘不足不再参与pod调度,但尝试回收磁盘空间时却发现已经没有active pod可以kill了!

二、原因分析

既然提到了磁盘不足,我们就来看看磁盘占用情况:

# df -h

文件系统        容量  已用  可用 已用% 挂载点
udev            2.0G     0  2.0G    0% /dev
tmpfs           396M   46M  350M   12% /run
/dev/sda1       5.8G  5.1G  448M   92% /
tmpfs           2.0G  288K  2.0G    1% /dev/shm
tmpfs           5.0M     0  5.0M    0% /run/lock
tmpfs           2.0G     0  2.0G    0% /sys/fs/cgroup
/dev/sdb1        99G  5.2G   89G    6% /data
tmpfs           396M     0  396M    0% /run/user/0
... ...

我们看到root分区的磁盘占用率已经达到了92%,仅剩下不到500M空间可以使用了。我们的私有云提供的ubuntu vm模板太过死板(无法定制),每个vm挂载的root分区只能是6G,多一点都不可以。这样在安装完一些必要的软件后,根分区占用率就很高了。为此,之前我们还特意挂载了一块专用盘(/dev/sdb1)用于存储docker的相关image和容器运行数据,并将原先的docker数据迁移到新位置(/data/docker)。

附:docker运行时数据迁移方法(适用于docker 1.12.x以后版本):
a) 创建/etc/docker/daemon.json

文件内容如下:
{
“graph”: “/data/docker”,
“storage-driver”: “aufs”
}

b) 停止docker并迁移数据
systemctl stop docker
mv /var/lib/docker /data

c) 重启docker
systemctl daemon-reload
systemctl restart docker

由于某些原因,我们的那个portal pod必须运行于该node上(通过nodeSelector选定node的方式)。在无法扩充根分区size的情况下,为了临时恢复pod运行,我们只能进一步“压榨”node了。于是我们的思路是:通过调整node的eviction threshold值来让node恢复healthy。

三、解决方案

要解决这一问题,我们需要阅读一下k8s官方的关于”Eviction Policy”的说明。大致意思就是:每个node上的kubelet都负责定期采集资源占用数据,并与预设的 threshold值进行比对,如果超过 threshold值,kubelet就会尝试杀掉一些Pod以回收相关资源,对Node进行保护。kubelet关注的资源指标threshold大约有如下几种:

- memory.available
- nodefs.available
- nodefs.inodesFree
- imagefs.available
- imagefs.inodesFree

每种threshold又分为eviction-soft和eviction-hard两组值。soft和hard的区别在于前者在到达threshold值时会给pod一段时间优雅退出,而后者则崇尚“暴力”,直接杀掉pod,没有任何优雅退出的机会。这里还要提一下nodefs和imagefs的区别:

  • nodefs: 指node自身的存储,存储daemon的运行日志等,一般指root分区/;
  • imagefs: 指docker daemon用于存储image和容器可写层(writable layer)的磁盘;

在我们的例子中,我们的imagefs是/dev/sdb1,磁盘占用率很低;而nodefs,即/分区占用率很高(92%)。

我们重启一次kubelet,查看一下这些threshold的当前值(通过journalctl -u kubelet -f查看):

10月 16 09:54:09 ubuntu-k8s-3 systemd[1]: Started kubelet: The Kubernetes Node Agent.
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.381711   12844 feature_gate.go:144] feature gates: map[]
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.437470   12844 client.go:72] Connecting to docker on unix:///var/run/docker.sock
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.438075   12844 client.go:92] Start docker client with request timeout=2m0s
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.471485   12844 manager.go:143] cAdvisor running in container: "/system.slice/kubelet.service"
... ...
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.615818   12844 container_manager_linux.go:246] container manager verified user specified cgroup-root exists: /
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.616263   12844 container_manager_linux.go:251] Creating Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName: KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/ CgroupDriver:cgroupfs ProtectKernelDefaults:false NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName: EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[] HardEvictionThresholds:[{Signal:memory.available Operator:LessThan Value:{Quantity:100Mi Percentage:0} GracePeriod:0s MinReclaim:<nil>} {Signal:nodefs.available Operator:LessThan Value:{Quantity:<nil> Percentage:0.1} GracePeriod:0s MinReclaim:<nil>} {Signal:nodefs.inodesFree Operator:LessThan Value:{Quantity:<nil> Percentage:0.05} GracePeriod:0s MinReclaim:<nil>}]} ExperimentalQOSReserved:map[]}
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.617680   12844 kubelet.go:263] Adding manifest file: /etc/kubernetes/manifests
10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.618196   12844 kubelet.go:273] Watching apiserver
... ...

把涉及到threshold的信息重新格式化一下:

    HardEvictionThresholds: [
        {
            Signal: memory.availableOperator: LessThanValue: {
                Quantity: 100MiPercentage: 0
            }GracePeriod: 0sMinReclaim: <nil>
        }{
            Signal: nodefs.availableOperator: LessThanValue: {
                Quantity: <nil>Percentage: 0.1
            }GracePeriod: 0sMinReclaim: <nil>
        }{
            Signal: nodefs.inodesFreeOperator: LessThanValue: {
                Quantity: <nil>Percentage: 0.05
            }GracePeriod: 0sMinReclaim: <nil>
        }
    ]

我们看到初始情况下,kubelet并没有设置Soft Eviction,只是对memory和nodefs设置了hard eviction threshold值。这里最值得我们关注的是:nodefs.available percentage: 0.1。也就是说当nodefs的可用空间低于10%时,该node上的kubelet将会执行eviction动作。而我们的根分区剩余可用空间为8%,显然满足了这个条件,于是问题就发生了。

我们要做的就是临时修改这个值,可以将其设为<5%。

四、解决步骤

我们需要为kubelet重新设定nodefs.available的threshold值。怎么做呢?

kubelet是运行于每个kubernetes node上的daemon,它在system boot时由systemd拉起:

root@ubuntu-k8s-3:~# ps -ef|grep kubelet
root      5718  5695  0 16:38 pts/3    00:00:00 grep --color=auto kubelet
root     13640     1  4 10:25 ?        00:17:25 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local --authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt --cadvisor-port=0

查看一下kubelet service的状态:

root@ubuntu-k8s-3:~# systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled)
  Drop-In: /etc/systemd/system/kubelet.service.d
           └─10-kubeadm.conf
   Active: active (running) since 一 2017-10-16 10:25:09 CST; 6h ago
     Docs: http://kubernetes.io/docs/
 Main PID: 13640 (kubelet)
    Tasks: 18
   Memory: 62.0M
      CPU: 18min 15.235s
   CGroup: /system.slice/kubelet.service
           ├─13640 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --
           └─13705 journalctl -k -f

.... ...

通过status的输出,我们看到关于kubelet service有两个systemd service配置文件与之启动相关:

- /lib/systemd/system/kubelet.service
Drop-In: /etc/systemd/system/kubelet.service.d
           └─10-kubeadm.conf

/lib/systemd/system/kubelet.service比较简单:

[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=http://kubernetes.io/docs/

[Service]
ExecStart=/usr/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10

[Install]
WantedBy=multi-user.target

/etc/systemd/system/kubelet.service.d/10-kubeadm.conf是systemd中用于override kubelet.service中部分配置的drop-in文件,kubelet的启动配置都在这里:

[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true"
Environment="KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true"
Environment="KUBELET_NETWORK_ARGS=--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
Environment="KUBELET_DNS_ARGS=--cluster-dns=10.96.0.10 --cluster-domain=cluster.local"
Environment="KUBELET_AUTHZ_ARGS=--authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt"
Environment="KUBELET_CADVISOR_ARGS=--cadvisor-port=0"
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_EXTRA_ARGS

systemd启动kubelet时会用10-kubeadm.conf中的ExecStart覆盖/lib/systemd/system/kubelet.service中的ExecStart,这样我们才能看到上面kubelet后面那一长溜命令行启动参数。我们要做的就是在这行启动参数后面添加上我们想设置的nodefs.available的threshold值。

出于配置风格一致的考量,我们定义一个新的Environment var,比如就叫:KUBELET_EVICTION_POLICY_ARGS:

Environment="KUBELET_EVICTION_POLICY_ARGS=--eviction-hard=nodefs.available<5%"
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_EXTRA_ARGS $KUBELET_EVICTION_POLICY_ARGS

重启kubelet,我们通过日志看threshold的新值是否生效:

10月 16 16:56:10 ubuntu-k8s-3 kubelet[7394]: I1016 16:56:10.840914    7394 container_manager_linux.go:251] Creating Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName: KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/ CgroupDriver:cgroupfs ProtectKernelDefaults:false NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName: EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[] HardEvictionThresholds:[{Signal:nodefs.available Operator:LessThan Value:{Quantity:<nil> Percentage:0.05} GracePeriod:0s MinReclaim:<nil>}]} ExperimentalQOSReserved:map[]}

我们看到下面这一行,表明新配置已经生效:

Signal:nodefs.available Operator:LessThan Value:{Quantity:<nil> Percentage:0.05}

查看pods状态,原先处于pending状态的三个pod均变成了”running”状态,问题得以解决。

五、参考资料

1) 《Handling Out of Resource Errors
2) 《Configure Out Of Resource Handling
3) 《Systemd 入门教程:实战篇
4) 《System bootup process
5) 《Systemd for upstart users- ubuntu wiki


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

Go语言:成长的十年

Go语言之父,Google大神Rob Pike代表Go语言的另外两位缔造者Robert GriesemerKen Thompson在自己的博客上发表了一篇名为《Go: Ten years and climbing》的文章,用以纪念Go语言从最初的设计idea起到目前的十年发展。笔者读完后,也是深有感触,因此在这里粗略翻译一下全文,希望能有更多的程序员加入到Gopher行列中来。

译文全文如下:

img{512x368}
Drawing Copyright ©2017 Renee French

本周是创建Go语言十周年的纪念日。

记得第一次关于这门语言设计的讨论是在2007年9月20日,一个周四的下午。进而在第二天的下午两点,我、Robert Griesemer以及Ken Thompson在谷歌山景城总部43#楼的一间名为Yaounde的会议室里又组织进行了一场有关这门语言设计的会议。这门语言的名字诞生于9月25日,在第一封有关语言设计的mail中可以看到一些关于命名的设计考量:

    Subject: Re: prog lang discussion
    From: Rob 'Commander' Pike
    Date: Tue, Sep 25, 2007 at 3:12 PM
    To: Robert Griesemer, Ken Thompson

    i had a couple of thoughts on the drive home.

    1. name

    'go'. you can invent reasons for this name but it has nice properties.
    it's short, easy to type. tools: goc, gol, goa. if there's an interactive
    debugger/interpreter it could just be called 'go'. the suffix is .go
    ...

(将语言命名为Go这事儿值得一提;“golang”来自于这门语言的web站点地址(因为go.com当时已经是迪斯尼的一个web站点了),但却不是语言的恰当名字。)

Go项目将2009年11月10日,即Go项目正式开源的那天作为其官方生日。最初Go项目托管在code.google.com上,几年后迁移至GitHub。不过,现在我们要回到最初的语言概念构建阶段,即那之前的两年,这可以让我们做更进一步地回顾,以更久远的视角,见证一些语言早期的历史事件。

Go开发过程中的第一个惊喜是收到下面这封mail信息:

    Subject: A gcc frontend for Go
    From: Ian Lance Taylor
    Date: Sat, Jun 7, 2008 at 7:06 PM
    To: Robert Griesemer, Rob Pike, Ken Thompson

    One of my office-mates pointed me at http://.../go_lang.html .  It
    seems like an interesting language, and I threw together a gcc
    frontend for it.  It's missing a lot of features, of course, but it
    does compile the prime sieve code on the web page.

Ian Lance Taylor的加入以及第二个编译器实现(gccgo)在带来震惊的同时,也伴随着喜悦。这对Go项目来说不仅仅是鼓励,更是一种对可行性的证明。有了语言的第二个实现对确定语言规范和标准库的过程是至关重要的,同时也有助于Go保证其高可移植性的承诺

虽然Ian的办公室离我们不远,但在看到这封mail之前我们从未谋面。不过,从那之后,Ian Lance Taylor便成为了Go语言及工具设计和实现的核心人物。

Russ Cox也是在2008年加入到刚成立不久的Go语言开发团队的。随着他的加入,他的一些天赋也随即在语言设计和实现中展现出来。Russ发现Go method的通用性意味着一个函数也可以拥有自己的方法,这直接导致了http.HandlerFunc的出现,这是一个我们所有人都未曾想到的结果。Russ还在当时设计的基础上提出了一些更泛化的想法,比如io.Readerio.Writer接口,奠定了所有I/O库的整体结构。

Jini Kim是我们最初的产品经理,他招来了安全专家Adam Langley来帮助我们将Go推向Google外面的世界。Adam为我们做了许多不为外人所知的事情,包括创建最初golang.org站点的web页面以及build dashboard。不过他最大的贡献当然要属cryptographic库了。起先,对于我们中的一部分人来说,这个库无论是规模还是复杂度,和其他库比起来都不成比例。但是就是这个库在后期成为了很多重要的网络和安全软件的基础,并且成为了Go语言开发历史的关键组成部分。像Cloudflare这样的网络基础设施提供商就重度依赖Adam在Go项目中的工作,Internet也因此变得更好。因此,我们由衷感谢他的工作。

事实上,许多公司在早期使用Go进行开发,尤其是初创公司。其中一些公司成为了云计算的巨头,其中就有一家这样的公司,它现在叫Docker。这家公司使用Go语言,并催化出计算领域的容器行业,进而导致了像Kubernetes这样的项目出现。今天我们可以说Go是容器语言,这是另一个我们完全没有预料到的结果。

不过,Go语言在云计算领域起到作用更大。2015年3月,Donnie Berkholz在为RedMonk撰写的一篇文章中宣称:Go是“云计算基础设施新兴语言”。几乎与此同时,Apcera的Derek Collison说:Go已经是云计算语言了。在那个时候,这也许还不是事实。但Berkholz所使用的“新兴”一词却恰如其分的表明了Go在当时的地位。

今天,Go已经成为云计算语言。想象一下:一个只有10岁的年轻编程语言已经成为这样一个规模庞大且不断发展的行业的主导者,这样的成功以前只是存在于在想象中。如果你觉得“主导”这个词太过强势的话,让我们来看看中国互联网行业。一段时间以来,Go在中国地区大量使用的数据一度让我们误认为Google趋势图出现了某些错误,但是凡是去过中国,参加过中国区Go语言大会的人都可以证实:Google趋势图的数据是真的,Go在中国的使用非常火爆!

简而言之,Go语言的十年发展为我们带来了许多里程碑。 最令人惊讶的是我们现在的位置:保守估计表明至少有50万Go程序员。 当前面那封为Go命名的邮件发送时,憧憬能有有五十万gopher的想法听起来会感觉很荒唐。 但就在此时此刻这里,我们不仅有了50w gopher,并且数量还在持续增长。

说到gophers,很高兴看到来自Renee French想法的吉祥物Go Gopher(地鼠),不仅成为了一个非常受人喜爱的作品,而且也是世界各地Go程序员的象征。许多各个地区顶级的Go大会都被称为GopherCons,因为他们聚集了来自世界各地的gophers。

Gopher大会正在迅速发展。第一次大会的举办只不过是三年前的事情,但今天在全世界各地有很多这样的Go大会。并且还有无数小的本地“聚会(meetups)”。在任何某一天,世界上某个地方都会有不止一个gopher群体在进行有关Go的分享。

回顾过去十年的Go设计和开发,Go社区的发展是惊人的。会议和聚会的数量、长长的且不断增加的Go项目贡献者名单、大量用Go实现的开放源代码存储库、使用Go的公司数量等等,细思恐(吃惊)极!

对于我们三个人,Robert, Rob和Ken,当初只是想让我们的编程生活更轻松一些,而如今,我们难以置信地、欣慰地看到我们的工作已经开始起作用了。

未来十年会带来什么呢?

- Rob Pike, with Robert Griesemer and Ken Thompson


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats