标签 Ubuntu 下的文章

TensorFlow入门:零基础建立第一个神经网络

首先,我不得不承认这篇文章有些标题党的味道^0^,但文章还是要继续写下去,备忘也好,能帮助到一些人也好。

2016小结的时候,我说过:2017年要了解一些有关机器学习人工智能(以下简称AI)方面的技术。如果有童鞋问:Why?我会告诉你:跟风。作为技术人,关注和紧跟业界最前沿的技术总是没错的。

2016年被业界普遍认为是AI这一波高速发展的元年,当然DeepMindAlphaGo在这方面所起到的作用是功不可没的。不过人工智能并未仅仅停留在实验室,目前可以说人工智能已经深入到我们生活中的方方面面,比如:电商的精准个性化商品推荐、手机上安装的科大讯飞的中文语音识别引擎以及大名鼎鼎的Apple的siri等。只是普通老百姓并没有意识到这一点,或者说当前AI的存在和运行形式与大家传统思想中的“AI”还未到形似的地步,再或者当前AI的智能程度还未让人们感觉到AI时代的到来。

人工智能是当前的技术风口,也是投资风口。不过,人工智能技术与普通的IT技术不同的地方在于其背后需要大量且有一定深度的数学理论知识,有门槛,并且门槛较高,这会让普通程序员望而却步的。还好有国际大公司,比如:Google、Facebook等在努力在降低这一门槛,让人工智能技术更加接地气,让更多从事IT领域的人能接触到AI,并思考如何利用AI解决实际问题。Google的TensorFlow应该就是在这样的背景下诞生的。

这里并不打算介绍TensorFlow是什么,其原理是什么(因为目前我也不知道),只是利用TensorFlow简简单单地建立起一个神经网络模型,带着大家感性的认知一下什么是AI。本文特别适合那些像我一样,从未接触过AI,但又想感性认识AI的程序员童鞋们。

一、由来

和AI门外的程序员童鞋一样,想窥探AI的世界已久,但苦于没有引路人,一直在门外徘徊。直到看到martin gorner的那篇《TensorFlow and deep learning, without a PhD》。在这篇文章中,martin已经将利用TensorFlow建立并一步步训练优化一个神经网络的门槛降低到了最简化的程度了。不过即便这样,把martin所使用这个环境搭建起来(文中虽然有详细步骤),可能依旧会遇到一些问题,本文的目的之一就是帮助你迈过这“最后一公里”。

二、搭建环境

我所使用的环境是一台think center x86_64物理机,安装的是ubuntu 16.04.1。相关软件版本:

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)   

$ git version
git version 2.7.4

按照教程中INSTALL.txt中的步骤,我们需要安装依赖软件:

$ sudo  apt-get install python3
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
python3 已经是最新版 (3.5.1-3)。
升级了 0 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 203 个软件包未被升级。

$ sudo apt-get install python3-matplotlib
python3-matplotlib 已经是最新版 (1.5.1-1ubuntu1)。

$sudo apt-get install python3-pip
python3-pip 已经是最新版 (8.1.1-2ubuntu0.4)。

$ pip3 install --upgrade tensorflow
Collecting tensorflow
  Downloading tensorflow-0.12.1-cp35-cp35m-manylinux1_x86_64.whl (43.1MB)
... ...
Installing collected packages: numpy, six, wheel, setuptools, protobuf, tensorflow
Successfully installed numpy-1.11.0 protobuf setuptools-20.7.0 six-1.10.0 tensorflow wheel-0.29.0

我们看到安装的TensorFlow是0.12.1版本,这应该是TensorFlow发布1.0版本前的最后一个Release版了。

下载Martin的教程代码:

$ mkdir -p ~/test/tensorflow

$ git clone https://github.com/martin-gorner/tensorflow-mnist-tutorial.git
正克隆到 'tensorflow-mnist-tutorial'...
remote: Counting objects: 271, done.
remote: Total 271 (delta 0), reused 0 (delta 0), pack-reused 271
接收对象中: 100% (271/271), 95.01 KiB | 46.00 KiB/s, 完成.
处理 delta 中: 100% (171/171), 完成.
检查连接... 完成。

我使用的tutorial的revision是:commit a9eb2bfcd74df4d7f3891d5403468d87547320e8。

三、建立并训练识别手写数字的神经网络

万事俱备,只差执行。

一起来建立我们的第一个神经网络:

$cd ~/test/tensorflow/tensorflow-mnist-tutorial
$ ls
cloudml          LICENSE                                mnist_2.2_five_layers_relu_lrdecay_dropout.py  mnist_4.1_batchnorm_five_layers_relu.py  tensorflowvisu_digits.py
CONTRIBUTING.md  mnist_1.0_softmax.py                   mnist_3.0_convolutional.py                     mnist_4.2_batchnorm_convolutional.py     tensorflowvisu.mplstyle
data             mnist_2.0_five_layers_sigmoid.py       mnist_3.1_convolutional_bigger_dropout.py      __pycache__                              tensorflowvisu.py
INSTALL.txt      mnist_2.1_five_layers_relu_lrdecay.py  mnist_4.0_batchnorm_five_layers_sigmoid.py     README.md                  

$ python3 mnist_1.0_softmax.py
/usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')
/usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')

Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting data/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting data/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting data/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting data/t10k-labels-idx1-ubyte.gz
Traceback (most recent call last):
  File "mnist_1.0_softmax.py", line 80, in <module>
    datavis = tensorflowvisu.MnistDataVis()
  File "/home/tonybai/test/tensorflow/tensorflow-mnist-tutorial/tensorflowvisu.py", line 166, in __init__
    self._color4 = self.__get_histogram_cyclecolor(histogram4colornum)
  File "/home/tonybai/test/tensorflow/tensorflow-mnist-tutorial/tensorflowvisu.py", line 160, in __get_histogram_cyclecolor
    colors = clist.by_key()['color']
AttributeError: 'Cycler' object has no attribute 'by_key'

出错了!

这里要注意的是:初次建立时,程序会首先从MNIST dataset下载训练数据文件,这里需要等待一段时间,千万别认为是程序出现什么hang住的异常情况。

之后的AttributeError才是真正的出错了!直觉告诉我是课程程序依赖的某个第三方库版本的问题,但又不知道是哪个库,于是我用临时处理方案fix it:

//tensorflowvisu.py
         #self._color4 = self.__get_histogram_cyclecolor(histogram4colornum)
         #self._color5 = self.__get_histogram_cyclecolor(histogram5colornum)
         self._color4 = '#CFF57F'
         self._color5 = '#E6C54A'

我把出错的调用注释掉,用hardcoding的方式直接赋值了两个color。

再次运行这个模型,我们终于看到那个展示训练过程的“高大上”的窗口弹了出来:

img{512x368}

运行一段时间后,当序号递增到2001时,程序hang住了。最初我以为是程序又出了错,最后在Martin的解释下,我才明白原来是训练结束了。在mnist_1.0_softmax.py文件末尾,我们可以看到这样一行注释:

# final max test accuracy = 0.9268 (10K iterations). Accuracy should peak above 0.92 in the first 2000 iterations.

这里告诉我们对神经网络的训练会进行多少次iterations。mnist_1.0_softmax.py需要2000次。tensorflow-mnist-tutorial下的每个训练程序文件末尾都有iteration次数,只不过有的说明简单些,有些复杂些罢了。

另外一个issue中,Martin也回应了上面的error问题,他的solution是:

pip3 install --upgrade matplotlib

我实测后,发现问题的确消失了!

四、小结

识别手写数字较为简单,采用softmax都可以将识别率训练到92%左右。采用其他几个模型,比如:mnist_4.1_batchnorm_five_layers_relu.py,可以将识别准确率提升到98%,甚至更高。

将这个教程运行起来的第一感觉就是AI真的很“高大上”,看着刷屏的日志和不断变化的UI,真有些科幻大片的赶脚,看起来也让你感觉心旷神怡。

不过目前仅仅停留在感性认知,深入理解TensorFlow背后的运行原理以及训练模型背后的理论才算是真正入门,这里仅仅是在AI领域迈出的一小步罢了^0^。

以Kubeadm方式安装的Kubernetes集群的探索

当前手上有两个Kubernetes cluster,一个是采用kube-up.sh安装的k8s 1.3.7版本,另外一个则是采用kubeadm安装的k8s 1.5.1版本。由于1.3.7版本安装在前,并且目前它也是承载了我们PaaS平台的环境,因此对于这个版本的Kubernetes安装环境、配置操作、日志查看、集群操作等相对较为熟悉。而Kubeadm安装的1.5.1版本K8s集群在组件部署、配置、日志等诸多方面与1.3.7版本有了较大差异。刚上手的时候,你会发现你原来所熟知的1.3.7的东西都不在原先的位置上了。估计很多和我一样,采用kubeadm将集群升级到1.5.1版本的朋友们都会遇到这类问题,于是这里打算对Kubeadm方式安装的Kubernetes集群进行一些小小的探索,把一些变动较大的点列出来,供大家参考。

一、环境

这里使用的依然是文章《使用Kubeadm安装Kubernetes》中安装完毕的Kubernetes 1.5.1集群环境,底层是阿里云ECS,操作系统是Ubuntu 16.04.1。网络用的是weave network

试验集群只有两个Node:一个master node和一个minion node。但Master node由于被taint了,因此它与minion node一样参与集群调度和承担负载。

二、核心组件的Pod化

Kubeadm安装的k8s集群与kube-up.sh安装集群相比,最大的不同应该算是kubernetes核心组件的Pod化,即:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、kube-discovery以及etcd等核心组件都运行在集群中的Pod里的,这颇有些CoreOS的风格。只有一个组件是例外的,那就是负责在node上与本地容器引擎交互的Kubelet。

K8s的核心组件Pod均放在kube-system namespace中,通过kubectl(on master node)可以查看到:

# kubectl get pods -n kube-system
NAME                                    READY     STATUS    RESTARTS   AGE
etcd-iz25beglnhtz                       1/1       Running   2          26d
kube-apiserver-iz25beglnhtz             1/1       Running   3          26d
kube-controller-manager-iz25beglnhtz    1/1       Running   2          26d
kube-scheduler-iz25beglnhtz             1/1       Running   4          26d
... ...

另外细心的朋友可能会发现,这些核心组建的Pod名字均以所在Node的主机名为结尾,比如:kube-apiserver-iz25beglnhtz中的”iz25beglnhtz”就是其所在Node的主机名。

不过,即便这些核心组件是一个容器的形式运行在集群中,组件所使用网络依然是所在Node的主机网络,而不是Pod Network

# docker ps|grep apiserver
98ea64bbf6c8        gcr.io/google_containers/kube-apiserver-amd64:v1.5.1            "kube-apiserver --ins"   10 days ago         Up 10 days                              k8s_kube-apiserver.6c2e367b_kube-apiserver-iz25beglnhtz_kube-system_033de1afc0844729cff5e100eb700a81_557d1fb2
4f87d22b8334        gcr.io/google_containers/pause-amd64:3.0                        "/pause"                 10 days ago         Up 10 days                              k8s_POD.d8dbe16c_kube-apiserver-iz25beglnhtz_kube-system_033de1afc0844729cff5e100eb700a81_5931e490

# docker inspect 98ea64bbf6c8
... ...
"HostConfig": {
"NetworkMode": "container:4f87d22b833425082be55851d72268023d41b50649e46c738430d9dfd3abea11",
}
... ...

# docker inspect 4f87d22b833425082be55851d72268023d41b50649e46c738430d9dfd3abea11
... ...
"HostConfig": {
"NetworkMode": "host",
}
... ...

从上面docker inspect的输出可以看出kube-apiserver pod里面的pause容器采用的网络模式是host网络,而以pause容器网络为基础的kube-apiserver 容器显然就继承了这一network namespace。因此从外面看,访问Kube-apiserver这样的组件和以前没什么两样:在Master node上可以通过localhost:8080访问;在Node外,可以通过master_node_ip:6443端口访问。

三、核心组件启动配置调整

在kube-apiserver等核心组件还是以本地程序运行在物理机上的时代,修改kube-apiserver的启动参数,比如修改一下–service-node-port-range的范围、添加一个–basic-auth-file等,我们都可以通过直接修改/etc/default/kube-apiserver(以Ubuntu 14.04为例)文件的内容并重启kube-apiserver service(service restart kube-apiserver)的方式实现。其他核心组件:诸如:kube-controller-manager、kube-proxy和kube-scheduler均是如此。

但在kubeadm时代,这些配置文件不再存在,取而代之的是和用户Pod描述文件类似的manifest文件(都放置在/etc/kubernetes/manifests下面):

/etc/kubernetes/manifests# ls
etcd.json  kube-apiserver.json  kube-controller-manager.json  kube-scheduler.json

我们以为kube-apiserver增加一个启动参数:”–service-node-port-range=80-32767″ 为例:

打开并编辑/etc/kubernetes/manifests/kube-apiserver.json,在“command字段对应的值中添加”–service-node-port-range=80-32767″:

"containers": [
      {
        "name": "kube-apiserver",
        "image": "gcr.io/google_containers/kube-apiserver-amd64:v1.5.1",
        "command": [
          "kube-apiserver",
          "--insecure-bind-address=127.0.0.1",
          "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota",
          "--service-cluster-ip-range=10.96.0.0/12",
          "--service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem",
          "--client-ca-file=/etc/kubernetes/pki/ca.pem",
          "--tls-cert-file=/etc/kubernetes/pki/apiserver.pem",
          "--tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem",
          "--token-auth-file=/etc/kubernetes/pki/tokens.csv",
          "--secure-port=6443",
          "--allow-privileged",
          "--advertise-address=10.47.217.91",
          "--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname",
          "--anonymous-auth=false",
          "--etcd-servers=http://127.0.0.1:2379",
          "--service-node-port-range=80-32767"
        ],

注意:不要忘记在–etcd-servers这一行后面添加一个逗号,否则kube-apiserver会退出。

修改后,你会发现kube-apiserver会被自动重启。这是kubelet的功劳。kubelet在启动时监听/etc/kubernetes/manifests目录下的文件变化并做适当处理:

# ps -ef|grep kubelet
root      1633     1  5  2016 ?        1-09:24:47 /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

kubelet自身是一个systemd的service,它的启动配置可以通过下面文件修改:

# cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
[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"
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_EXTRA_ARGS

四、kubectl的配置

kube-up.sh安装的k8s集群会在每个Node上的~/.kube/下创建config文件,用于kubectl访问apiserver和操作集群使用。但在kubeadm模式下,~/.kube/下面的内容变成了:

~/.kube# ls
cache/  schema/

于是有了问题1:config哪里去了?

之所以在master node上我们的kubectl依旧可以工作,那是因为默认kubectl会访问localhost:8080来与kube-apiserver交互。如果kube-apiserver没有关闭–insecure-port,那么kubectl便可以正常与kube-apiserver交互,因为–insecure-port是没有任何校验机制的。

于是又了问题2:如果是其他node上的kubectl与kube-apiserver通信或者master node上的kubectl通过secure port与kube-apiserver通信,应该如何配置?

接下来,我们一并来回答上面两个问题。kubeadm在创建集群时,在master node的/etc/kubernetes下面创建了两个文件:

/etc/kubernetes# ls -l
total 32
-rw------- 1 root root 9188 Dec 28 17:32 admin.conf
-rw------- 1 root root 9188 Dec 28 17:32 kubelet.conf
... ...

这两个文件的内容是完全一样的,仅从文件名可以看出是谁在使用。比如kubelet.conf这个文件,我们就在kubelet程序的启动参数中看到过:–kubeconfig=/etc/kubernetes/kubelet.conf

# ps -ef|grep kubelet
root      1633     1  5  2016 ?        1-09:26:41 /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

打开这个文件,你会发现这就是一个kubeconfig文件,文件内容较长,我们通过kubectl config view来查看一下这个文件的结构:

# kubectl --kubeconfig /etc/kubernetes/kubelet.conf config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: REDACTED
    server: https://{master node local ip}:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: admin
  name: admin@kubernetes
- context:
    cluster: kubernetes
    user: kubelet
  name: kubelet@kubernetes
current-context: admin@kubernetes
kind: Config
preferences: {}
users:
- name: admin
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED
- name: kubelet
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED

这和我们在《Kubernetes集群Dashboard插件安装》一文中介绍的kubeconfig文件内容并不二致。不同之处就是“REDACTED”这个字样的值,我们对应到kubelet.conf中,发现每个REDACTED字样对应的都是一段数据,这段数据是由对应的数字证书内容或密钥内容转换(base64)而来的,在访问apiserver时会用到。

我们在minion node上测试一下:

minion node:

# kubectl get pods
The connection to the server localhost:8080 was refused - did you specify the right host or port?

# kubectl --kubeconfig /etc/kubernetes/kubelet.conf get pods
NAME                         READY     STATUS    RESTARTS   AGE
my-nginx-1948696469-359d6    1/1       Running   2          26d
my-nginx-1948696469-3g0n7    1/1       Running   3          26d
my-nginx-1948696469-xkzsh    1/1       Running   2          26d
my-ubuntu-2560993602-5q7q5   1/1       Running   2          26d
my-ubuntu-2560993602-lrrh0   1/1       Running   2          26d

kubeadm创建k8s集群时,会在master node上创建一些用于组件间访问的证书、密钥和token文件,上面的kubeconfig中的“REDACTED”所代表的内容就是从这些文件转化而来的:

/etc/kubernetes/pki# ls
apiserver-key.pem  apiserver.pem  apiserver-pub.pem  ca-key.pem  ca.pem  ca-pub.pem  sa-key.pem  sa-pub.pem  tokens.csv
  • apiserver-key.pem:kube-apiserver的私钥文件
  • apiserver.pem:kube-apiserver的公钥证书
  • apiserver-pub.pem kube-apiserver的公钥文件
  • ca-key.pem:CA的私钥文件
  • ca.pem:CA的公钥证书
  • ca-pub.pem :CA的公钥文件
  • sa-key.pem :serviceaccount私钥文件
  • sa-pub.pem :serviceaccount的公钥文件
  • tokens.csv:kube-apiserver用于校验的token文件

在k8s各核心组件的启动参数中会看到上面文件的身影,比如:

 kube-apiserver --insecure-bind-address=127.0.0.1 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota --service-cluster-ip-range=10.96.0.0/12 --service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem --client-ca-file=/etc/kubernetes/pki/ca.pem --tls-cert-file=/etc/kubernetes/pki/apiserver.pem --tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --token-auth-file=/etc/kubernetes/pki/tokens.csv --secure-port=6443 --allow-privileged --advertise-address={master node local ip} --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --anonymous-auth=false --etcd-servers=http://127.0.0.1:2379 --service-node-port-range=80-32767

我们还可以在minion node上通过curl还手工测试一下通过安全通道访问master node上的kube-apiserver。在《Kubernetes集群的安全配置》一文中,我们提到过k8s的authentication(包括:客户端证书认证、basic auth、static token等)只要通过其中一个即可。当前kube-apiserver开启了客户端证书认证(–client-ca-file)和static token验证(–token-auth-file),我们只要通过其中一个,就可以通过authentication,于是我们使用static token方式。static token file的内容格式:

token,user,uid,"group1,group2,group3"

对应到master node上的tokens.csv

# cat /etc/kubernetes/pki/tokens.csv
{token},{user},812ffe41-cce0-11e6-9bd3-00163e1001d7,system:kubelet-bootstrap

我们用这个token通过curl与apiserver交互:

# curl --cacert /etc/kubernetes/pki/ca.pem -H "Authorization: Bearer {token}"  https://{master node local ip}:6443
{
  "paths": [
    "/api",
    "/api/v1",
    "/apis",
    "/apis/apps",
    "/apis/apps/v1beta1",
    "/apis/authentication.k8s.io",
    "/apis/authentication.k8s.io/v1beta1",
    "/apis/authorization.k8s.io",
    "/apis/authorization.k8s.io/v1beta1",
    "/apis/autoscaling",
    "/apis/autoscaling/v1",
    "/apis/batch",
    "/apis/batch/v1",
    "/apis/batch/v2alpha1",
    "/apis/certificates.k8s.io",
    "/apis/certificates.k8s.io/v1alpha1",
    "/apis/extensions",
    "/apis/extensions/v1beta1",
    "/apis/policy",
    "/apis/policy/v1beta1",
    "/apis/rbac.authorization.k8s.io",
    "/apis/rbac.authorization.k8s.io/v1alpha1",
    "/apis/storage.k8s.io",
    "/apis/storage.k8s.io/v1beta1",
    "/healthz",
    "/healthz/poststarthook/bootstrap-controller",
    "/healthz/poststarthook/extensions/third-party-resources",
    "/healthz/poststarthook/rbac/bootstrap-roles",
    "/logs",
    "/metrics",
    "/swaggerapi/",
    "/ui/",
    "/version"
  ]
}

交互成功!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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