标签 云计算 下的文章

现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用

年初,火得发烫的独角兽IT公司Docker发布了一款新的企业级产品 Docker Datacenter (简称:DDC)。作 为拥有原生Docker容器技术的公司,其每个市场动作都会让轻量级容器生态圈内的公司不敢小觑。而要揣度Docker对商业改变的理解、对容器 技术栈应用的理解以及对新产品和服务在生态圈中的定位,就有必要对Docker的这款产品做一些比较深刻的了解。而其技术白皮书 恰是我们了解 Docker该产品的入口。这里我就基于自己对容器相关技术栈的粗浅理解,翻译一下这篇篇幅不长的技术白皮书,希望能给大家带来些许帮助。

标题:现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用

译文全文如下:

摘要

开发人员不接受被锁住的平台。就像《金发小女孩和三只熊》 故事那样,开发人员们一直在为其开发环境寻找一种可以在自由和约束之间拥 有最佳平衡的权力。在这个过程中,他们发现“平台即服务”(PaaS)模型层次太高、过于抽象以及约束过多,并且为了实现一个完全锁定的、黑盒的 环境而牺牲了灵活性;同时,他们也发现“基础设施即服务”(IaaS)模型提供的各自的容器服务也是不够的,因为那种服务仅驻留在各自的基础设施 中,缺乏远见。在寻求适当方案的过程中,一些组织开始提供基于Docker的“容器即服务”(CaaS)的环境,这种模型为开发团队提供了敏捷 性;为运维团队提供了控制力;为应用程序提供了跨基础设施的可移植性 — 从本地数据中心到公有云,横跨诸多网络和存储设备供应商。

Docker平台为基础设施无关的CaaS模型提供了一套集成套件。使用这个方案,IT运维团队既可以对基础设施,也可以对基础应用内容进行安全 保护、配置和管理;同时开发人员也能够以自助的方式来构建和部署他们的应用。

在本白皮书中,我们将讨论新软件模型的驱动力,Docker平台的能力,细化CaaS的需求,以及详细说明在构建、交付(运输)和运行应用程序过 程中解决核心问题的重要性。

重要结论包括:

• 云、数据和微服务是如何改变商业的
• 理解Docker的发展历程
• Docker CaaS模型的能力与优势

一、通过软件改变商业

运行成品软件的私有数据中心以及一年更新一次的巨大单一代码库的时代已经离我们远去了。一切都在变化。不管是迁移到云上,在云间移植,用现代化的 方法改造遗留程序,还是构建新的应用和数据结构,我们想要的结果都是相同的 – 速度。你动作的越快,你的公司将会越成功。

软件是定义你的公司的关键IP(知识产权),即便你的公司实际出售的商品可能只是一件T恤、一辆车或复利(compounding interest)。软件就是你如何接洽客户,如何吸引新用户,如何理解他们的数据,如何推广你的产品或服务以及如何处理他们的订单。

要做好这些,当今的软件正趋向定制化。为一个非常具体的工作而设计的软件片段被称为微服务(microservice)。微服务的设计目标是让 每一个由必要组件构建出来的服务在适当类型的底层基础设施资源上运行一个特定的工作(job)。接下来,这些服务松耦合在一起,可以随时被修改, 无需担心服 务运行的先后次序。

这种方法,虽然对持续改进十分有利,但在达成最终结果的过程中也提出了许多挑战。首先,它创建了一个新的、不断膨胀的服务、依赖和基础设施矩阵, 让它自身很难于管理。此外,它没有考虑到眼前大量已经存在的遗留程序,完全异构的应用程序栈以及实际中必须保证运行起来的进程。

二、Docker的发展历程以及AND的力量

2013年,Docker以具备构建、交付、到处运行的应用容器而出现在大众视野当中。与今天集装箱的运输类似,软件容器就是一个软件的标准单 元,不管容器内存放的代码和依赖是什么,容器外部看起来都相同。这使得开发人员和系统管理员可以跨基础设施和各种各样环境传输容器,而无需做任何 修改和考虑不同环境下的不同配置。Docker的历程就从此时开始了。

敏捷性: Docker的速度和简洁让Docker一经推出便大受开发者欢迎,同时也使得其开源项目的热度以流星般速度蹿升。现在开发者能很容易地将软件以及其依赖 打包到一个容器中。开发者可以使用任何语言、版本和工具,因为这些都被打包到一个容器中,容器将所有异质性标准化了,并且无需付出任何代价。

可移植性: Docker技术的本质让那批开发者意识到他们的应用容器现在可移植了,而且是以在以前不可能的方式。他们可以将应用从开发环境直接交付到测试和产品环境 且代码总是按设计那样正常工作。环境中的任何差异都不会影响到容器里面的应用。应用也无需修改就可以正常工作在生产环境中。这同样也是IT运维团 队的一个福音,因为现在他们可以跨数据中心迁移应用来避免厂商的平台锁定了。

控制: 当应用程序沿着通往生产环境的生命周期前进时,关于安全性、可管理性以及伸缩性等新问题需要进一步得到解答。Docker标准化了你的环境,同时维护着你 的业务所需的异质性。Docker提供了设置适当控制级别的能力以及维护服务级别、性能以及监管的灵活性。IT运维组能够通过供应、安全加固、监 控和伸缩基础设施和应用来保持峰值服务水平。没有两个程序或业务是一样的,Docker允许你决定如何去控制你的应用环境。

Docker成长历程的核心是AND的力量。Docker是唯一一个可以跨应用生命周期所有阶段,为开发者和IT运维团队在提供敏捷性、可移植性 和控制的方案。从这些核心原则来看,CaaS的脱颖而出正是由于由其构建的新应用又好又快。

三、Docker Containers as a Service(CaaS)

容器即服务(CaaS)是什么?它是基于基础设施和内容的一个IT受控的、安全的应用环境,利用它开发人员可以以自助的方式构建和部署应用。

img{512x368}

在上面的CaaS图示中,开发和IT运维团队通过registry相互协作。registry服务用于维护一个安全的、经过签名的映像仓库。左边 的开发者通过registry服务可以将软件拉(pull)到本地,按自己的步伐构建软件。当软件通过集成测试,开发者将其内容推回(push back)registry以保存最新版本。部署步骤因内部过程的不同而异,既可以通过工具自动进行,也可以是人工部署。

上图中右侧的IT运维组为生产环境基础设施管理着不同供应商的合同,诸如:计算、网络和存储。这些团队负责提供应用所需的计算资源,使用 Docker Universal Control Plane随时随地监控集群和应用。他们能在云间迁移应用,或伸缩服务来维持峰值服务水平。

四、关键特性和考量

Docker CaaS为组织提供了一套框架用于统一他们环境中的各种系统、语言和工具,并为业务提供所需的控制、安全或特权级别。由于是一种支持全部Docker API的Docker原生方案,Docker CaaS能够无缝地将应用从本地开发环境部署到生产环境,而无需改变代码或简化部署周期。

以下特性组成了组织应用环境的最低需求。在这个范式中,开发和运维团队被授权使用各自最佳的工具,而无需担心对系统、其他人的工作流或锁定状态造 成破坏。

1、开发者和运维的需求。 许多工具仅能解决针对一个团队的功能需求,但CaaS打破了持续改进的周期。为了获得从开发到生产环境运行的真正加速,你需要在一个连续周期内同时满足两类用户的需求。Docker为每个团队都提供了独特的能力,同时还提供了横跨整个平台的一致的API,保证了从一个团队到另外一个团队的无缝过渡。

2、应用程序生命周期的所有阶段。 从持续集成到持续交付以及开发运维(devops),这些实践都是为了消除瀑布开发方法以及其带来的滞后的周期。通过给开发和运维团队提供工具,Docker可以无缝的支持应用从构建、测试到部署到生产环境运行的所有阶段。

3、任何语言。开发者敏捷性意味着开发者在构建他们的应用的时候可以自由选择使用任何应用特性需要的编程语言、版本和工具。同时,在同一时间运行一个语言的多个版本的能力也为开发者提供了极大的灵活性。Docker让你的团队更加关注于构建应用程序本身,而不是思考如何构建一个可以在Docker中运行的应用。

4、任何操作系统。 绝大多数的组织拥有不止一款操作系统。一些工具在Linux上工作的更好,而另外一些可能在Windows上运行的更优异。应用平台需要考虑和支持这种多样性。否则,只能算是解决了部分问题而已。Docker起初是为Linux社区量身打造的,但Docker和微软公司正着手在Windows Server上实现Docker,以支持数百万现存企业应用以及未来企业应用。

5、任何基础设施。 谈到基础设施,组织想要的是选择、备份和杠杆作用。这是否意味着你需要拥有多个私有数据中心,一个混合云或者多个云提供商呢,其实关键点在于具备将应用负荷在不同环境间迁移而又不出问题的能力。Docker技术架构将基础设施与应用分离,使得应用容器可以在横跨基础设施在任意基础设施上运行。

6、Open API,插件式架构和生态系统。 一个平台不能算作是一个真正的平台,如果它只是一个封闭的孤岛。如果你想首先改良更新你现有的环境,通过实现新技术一般是不可行的。Docker的一个基本指导原则就是一个开放的平台。开放意味着API和插件可以让你利用上你已有的投资并让Docker适应你的环境和过程。开放性可以让生态系统更加活跃,且当你的CaaS增加特定功能时,它可以给你提供更多的灵活性和更多的选择。

虽然CaaS具有许多特性,但上述这些特性却是关键的,因为这种新的定制化应用范式只是为你的技术架构引入了更多异质性。Docker CaaS平台根本上就是为了支持这种多样性而设计的,并且针对任意规模提供相应的控制能力。

五、Docker CaaS

平台组件: Docker CaaS平台由一系列集成软件方案以及一个灵活的部署模型组成,以满足你的业务需求。

img{512x368}

本地数据中心/虚拟私有云(VPC): 对于那些要使用自己网络的组织,Docker Trusted Registry和Docker Universal Control Plan可以被部署在本地数据中心或虚拟私有云中,并且可以连接你已有的基础设施以及系统,比如存储、Active Directory/LDAP以及监控与日志解决方案。映像文件存储在你自己的存储架构中,Trusted Registry提供存储和管理服务能力,并且同时提供基于角色的对映像的基本访问控制。Universal Control Plane提供对Docker环境的可视化管理,包括Swarm集群、Trusted Registry仓库,容器以及多容器应用。

在云中: 对于那些接受使用SaaS方案的组织来说,Docker Hub和Docker Cloud提供了基于Docker上运行和管理的registry和control plane服务。Hub是一个云Registry服务,用于存储和管理映像文件以及用户权限。Docker Cloud供应和管理部署集群,同时也监控和管理已部署应用。使用Docker Cloud连接到你选择的云基础设施或使用你自己的物理节点来部署你的应用吧。

你的Docker CaaS可以设计成集中控制和管理,也可以设计成分布式管理以授权给各自应用团队。这种灵活性使得你可以建立一个最适合你的业务的模型,就像你选择基础设施和内容实现过程那样。CaaS是构建、交付和运行应用理念的一个延伸。

事实上由于CaaS统一了跨环境的本质,加速了许多IT倡议被接纳的过程。每个组织都有其自己采纳的倡议:从容器化,包括对已有应用的改造和迁移,到微服务,再到持续集成、持续交付和devops以及对各类云的接纳、迁移、混合及支持多种云。在每个场景中,Docker CaaS都能带来敏捷性、可移植性和控制,使得组织能接受那些用例。

六、AND的力量

总之,云、应用和数据的变化已经将技术和商业之间的对话,从“你如何帮我削减成本”换成了“你如何加速我的商业”。当你踏上你的旅途时,Docker提供了额外的灵活性帮你选择在哪里存储你的应用内容以及在哪里部署你的控制台。让你的CaaS适配你的业务需求,不管是部署在本地数据中心或虚拟私有云上,还是作为云服务被平滑地消费。无论你的业务是什么,Docker CaaS平台都会提供敏捷性、可移植性和控制力,尽可能又快又好的构建最好的应用,以最优的代价提供峰值性能的服务,并且不会被平台锁定。

Appdash,用Go实现的分布式系统跟踪神器

在“云”盛行的今天,分布式系统已不是什么新鲜的玩意儿。用脚也能想得出来:Google、baidu、淘宝、亚马逊、twitter等IT巨头 背后的巨型计算平台都是分布式系统了,甚至就连一个简单的微信公众号应用的后端也都分布式了,即便仅有几台机器而已。分布式让系统富有弹性,面 对纷繁变化的需求,可以伸缩自如。但分布式系统也给开发以及运维人员带来了难题:如何监控和优化分布式系统的行为。

以google为例,想象一下,用户通过浏览器发起一个搜索请求,Google后端可能会有成百上千台机器、多种编程语言实现的几十个、上百个应 用服务开始忙碌起来,一起计算请求的返回结果。一旦这个过程中某一个环节出现问题/bug,那么查找和定位起来是相当困难的,于是乎分布式系统跟 踪系统出炉了。Google在2010年发表了著名论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》(中文版在这里)。Dapper是google内部使用的一个分布式系统跟踪基础设施,与之前的一些跟踪系统相比,Dapper以低消耗、对应用透明以及良好的扩展性著称。并且 Google Dapper更倾向于性能数据方面的收集和调查,可以辅助开发人员和运维人员发现分布式系统的性能瓶颈并着手优化。Dapper出现后,各大巨头开始跟 风,比如twitter的Zipkin(开源)、淘宝的“鹰眼”、eBay的Centralized Activity Logging (CAL)等,它们基本上都是参考google的dapper论文设计和实现的。

而本文将要介绍的Appdash则是sourcegraph开源的一款用Go实现的分布式系统跟踪工具套件,它同样是以google的 dapper为原型设计和实现的,目前用于sourcegraph平台的性能跟踪和监控。

一、原理

Appdash实现了Google dapper中的四个主要概念:

【Span】

Span指的是一个服务调用的跨度,在实现中用SpanId标识。根服务调用者的Span为根span(root span),在根级别进行的下一级服务调用Span的Parent Span为root span。以此类推,服务调用链构成了一棵tree,整个tree构成了一个Trace。

Appdash中SpanId由三部分组成:TraceID/SpanID/parentSpanID,例如: 34c31a18026f61df/aab2a63e86ac0166/592043d0a5871aaf。TraceID用于唯一标识一次Trace。traceid在申请RootSpanID时自动分配。

在上面原理图中,我们也可以看到一次Trace过程中SpanID的情况。图中调用链大致是:

frontservice:
        call  serviceA
        call  serviceB
                  call serviceB1
        … …
        call  serviceN

对应服务调用的Span的树形结构如下:

frontservice: SpanId = xxxxx/nnnn1,该span为root span:traceid=xxxxx, spanid=nnnn1,parent span id为空。
serviceA: SpanId = xxxxx/nnnn2/nnnn1,该span为child span:traceid=xxxxx, spanid=nnnn2,parent span id为root span id:nnnn1。
serviceB: SpanId = xxxxx/nnnn3/nnnn1,该span为child span:traceid=xxxxx, spanid=nnnn3,parent span id为root span id:nnnn1。
… …
serviceN: SpanId = xxxxx/nnnnm/nnnn1,该span为child span:traceid=xxxxx, spanid=nnnnm,parent span id为root span id:nnnn1。
serviceB1: SpanId = xxxxx/nnnn3-1/nnnn3,该span为serviceB的child span,traceid=xxxxx, spanid=nnnn3-1,parent span id为serviceB的spanid:nnnn3

【Event】

个人理解在Appdash中Event是服务调用跟踪信息的wrapper。最终我们在Appdash UI上看到的信息,都是由event承载的并且发给Appdash Server的信息。在Appdash中,你可以显式使用event埋点,吐出跟踪信息,也可以使用Appdash封装好的包接口,比如 httptrace.Transport等发送调用跟踪信息,这些包的底层实现也是基于event的。event在传输前会被encoding为 Annotation的形式。

【Recorder】

在Appdash中,Recorder是用来发送event给Appdash的Collector的,每个Recorder会与一个特定的span相关联。

【Collector】

从Recorder那接收Annotation(即encoded event)。通常一个appdash server会运行一个Collector,监听某个跟踪信息收集端口,将收到的信息存储在Store中。

二、安装

appdash是开源的,通过go get即可得到源码并安装example:

go get -u sourcegraph.com/sourcegraph/appdash/cmd/…

appdash自带一个example,在examples/cmd/webapp下面。执行webapp,你会看到如下结果:

$webapp
2015/06/17 13:14:55 Appdash web UI running on HTTP :8700
[negroni] listening on :8699

这是一个集appdash server, frontservice, fakebackendservice于一身的example,其大致结构如下图:

通过浏览器打开:localhost:8700页面,你会看到appdash server的UI,通过该UI你可以看到所有Trace的全貌。

访问http://localhost:8699/,你就触发了一次Trace。在appdash server ui下可以看到如下画面:

从页面上展示的信息可以看出,该webapp在处理用户request时共进行了三次服务调用,三次调用的耗时分别为:201ms,202ms, 218ms,共耗时632ms。

一个更复杂的例子在cmd/appdash下面,后面的应用实例也是根据这个改造出来的,这里就不细说了。

三、应用实例

这里根据cmd/appdash改造出一个应用appdash的例子,例子的结构如下图:

例子大致分为三部分:
appdash — 实现了一个appdash server, 该server带有一个collector,用于收集跟踪信息,收集后的信息存储在一个memstore中;appdash server提供ui,ui从memstore提取信息并展示在ui上供operator查看。
backendservices — 实现两个模拟的后端服务,供frontservice调用。
frontservice — 服务调用的起始端,当用户访问系统时触发一次跟踪。

先从backendservice这个简单的demo service说起,backendservice下有两个service: ServiceA和ServiceB,两个service几乎一模一样,我们看一个就ok了:

//appdash_examples/backendservices/serviceA.go
package main

import (
    "fmt"
    "net/http"
    "time"
)

func handleRequest(w http.ResponseWriter, r *http.Request) {
    var err error
    if err = r.ParseForm(); err != nil {
        fmt.Println("Http parse form err:", err)
        return
    }
    fmt.Println("SpanId =", r.Header.Get("Span-Id"))

    time.Sleep(time.Millisecond * 101)
    w.Write([]byte("service1 ok"))
}

func main() {
    http.HandleFunc("/", handleRequest)
    http.ListenAndServe(":6601", nil)
}

这是一个"hello world"级别的web server。值得注意的只有两点:
1、在handleRequest中我们故意Sleep 101ms,用来模拟服务的耗时。
2、打印出request头中的"Span-Id"选项值,用于跟踪Span-Id的分配情况。

接下来我们来看appdash server。appdash server = collector +store +ui。

//appdash.go
var c Server

func init() {
    c = Server{
        CollectorAddr: ":3001",
        HTTPAddr:      ":3000",
    }
}

type Server struct {
    CollectorAddr string
    HTTPAddr      string
}

func main() {
    var (
        memStore = appdash.NewMemoryStore()
        Store    = appdash.Store(memStore)
        Queryer  = memStore
    )

    app := traceapp.New(nil)
    app.Store = Store
    app.Queryer = Queryer

    var h http.Handler = app
    var l net.Listener
    var proto string
    var err error
    l, err = net.Listen("tcp", c.CollectorAddr)
    if err != nil {
        log.Fatal(err)
    }
    proto = "plaintext TCP (no security)"
    log.Printf("appdash collector listening on %s (%s)",
                c.CollectorAddr, proto)
    cs := appdash.NewServer(l, appdash.NewLocalCollector(Store))
    go cs.Start()

    log.Printf("appdash HTTP server listening on %s", c.HTTPAddr)
    err = http.ListenAndServe(c.HTTPAddr, h)
    if err != nil {
        fmt.Println("listenandserver listen err:", err)
    }
}

appdash中的Store是用来存储收集到的跟踪结果的,Store是Collector接口的超集,这个例子中,直接利用memstore(实现了 Collector接口)作为local collector,利用store的Collect方法收集trace数据。UI侧则从store中读取结果展示给用户。

最后我们说说:frontservice。frontservice是Trace的触发起点。当用户访问8080端口时,frontservice调用两个backend service:

//frontservice.go
func handleRequest(w http.ResponseWriter, r *http.Request) {
    var result string
    span := appdash.NewRootSpanID()
    fmt.Println("span is ", span)
    collector := appdash.NewRemoteCollector(":3001")

    httpClient := &http.Client{
        Transport: &httptrace.Transport{
            Recorder: appdash.NewRecorder(span, collector),
            SetName:  true,
        },
    }

    //Service A
    resp, err := httpClient.Get("http://localhost:6601")
    if err != nil {
        log.Println("access serviceA err:", err)
    } else {
        log.Println("access serviceA ok")
        resp.Body.Close()
        result += "access serviceA ok\n"
    }

    //Service B
    resp, err = httpClient.Get("http://localhost:6602")
    if err != nil {
        log.Println("access serviceB err:", err)
        return
    } else {
        log.Println("access serviceB ok")
        resp.Body.Close()
        result += "access serviceB ok\n"
    }
    w.Write([]byte(result))
}

func main() {
    http.HandleFunc("/", handleRequest)
    http.ListenAndServe(":8080", nil)
}

从代码看,处理每个请求时都会分配一个root span,同时traceid也随之分配出来。例子中没有直接使用Recorder埋点发送event,而是利用了appdash封装好的 httptrace.Transport,在初始化httpClient时,将transport实例与span和一个remoteCollector想 关联。后续每次调用httpClient进行Get/Post操作时,底层代码会自动调用httptrace.Transport的RoundTrip方 法,后者在Request header上添加"Span-Id"参数,并调用Recorder的Event方法将跟踪信息发给RemoteCollector:

//appdash/httptrace/client.go
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
    var transport http.RoundTripper
    if t.Transport != nil {
        transport = t.Transport
    } else {
        transport = http.DefaultTransport
    }

    … …
    req = cloneRequest(req)

    child := t.Recorder.Child()
    if t.SetName {
        child.Name(req.URL.Host)
    }
    SetSpanIDHeader(req.Header, child.SpanID)

    e := NewClientEvent(req)
    e.ClientSend = time.Now()

    // Make the HTTP request.
    resp, err := transport.RoundTrip(req)

    e.ClientRecv = time.Now()
    if err == nil {
        e.Response = responseInfo(resp)
    } else {
        e.Response.StatusCode = -1
    }
    child.Event(e)

    return resp, err
}

这种方法在一定程度上实现了trace对应用的透明性。

你也可以显式的在代码中调用Recorder的Event的方法将trace信息发送给Collector,下面是一个fake SQLEvent的跟踪发送:

 // SQL event
    traceRec := appdash.NewRecorder(span, collector)
    traceRec.Name("sqlevent example")

    // A random length for the trace.
    length := time.Duration(rand.Intn(1000)) * time.Millisecond
    startTime := time.Now().Add(-time.Duration(rand.Intn(100)) * time.Minute)
    traceRec.Event(&sqltrace.SQLEvent{
        ClientSend: startTime,
        ClientRecv: startTime.Add(length),
        SQL:        "SELECT * FROM table_name;",
        Tag:        fmt.Sprintf("fakeTag%d", rand.Intn(10)),
    })

不过这种显式埋点需要程序配合做一些改造。

四、小结

目前Appdash的资料甚少,似乎只是其东家sourcegraph在production环境有应用。在github.com上受到的关注度也不算高。

appdash是参考google dapper实现的,但目前来看appdash只是实现了“形”,也许称为神器有些言过其实^_^。

首先,dapper强调对应用透明,并使用了Thread LocalStorage。appdash实现了底层的recorder+event机制,上层通过httptrace、sqltrace做了封装,以降 低对应用代码的侵入性。但从上面的应用来看,透明性还有很大提高空间。

其次,appdash的性能数据、扩展方案sourcegraph并没有给出明确说明。

不过作为用go实现的第一个分布式系统跟踪工具,appdash还是值得肯定的。在小规模分布式系统中应用对于系统行为的优化还是会有很大帮助的。   

BTW,上述例子的完整源码在这里可以下载到。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系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