0%

持续集成与持续交付 - 使用Drone构建镜像_drone end docker push-CSDN博客

Excerpt

文章浏览阅读2.3k次。使用Drone构建镜像如何使用Gogs和Drone创建用于构建Docker映像的CI / CD流水线。步骤1 - 配置GogsGogs是一个自托管的Git服务,Drone是一个持续交付平台。它们可以建立CI/CD流水线,来构建Docker镜像。部署时,Drone服务的URL是必须的。运行docker-compose启动两个服务$ DRONE_HOST=https://2886795314-80-kitek05.environments.katacoda.com docker-compose up _drone end docker push


使用Drone构建镜像

如何使用Gogs和Drone创建用于构建Docker映像的CI / CD流水线。

步骤1 - 配置Gogs
Gogs是一个自托管的Git服务,Drone是一个持续交付平台。它们可以建立CI/CD流水线,来构建Docker镜像。部署时,Drone服务的URL是必须的。
运行docker-compose启动两个服务

1
$ DRONE_HOST=https://2886795314-80-kitek05.environments.katacoda.com docker-compose up -d Creating network "root_default" with the default driver Creating volume "root_db-data" with local driver Creating volume "root_gogs-data" with local driver Pulling postgres (postgres:9.5)... 9.5: Pulling from library/postgres 62deabe7a6db: Pull complete 24c7b6331486: Pull complete 00b83e6d4b38: Pull complete 0e54c5150202: Pull complete 31dd3ef3701c: Pull complete a7a29ab258fa: Pull complete 0d30412e41ab: Pull complete f300ff05c105: Pull complete 6087bca444de: Pull complete 9a074670548c: Pull complete 487595f7c39d: Pull complete 317bb2885f18: Pull complete e073a2b48253: Pull complete d43ef7481147: Pull complete Digest: sha256:8679e0d0658ec38b4ade78da75fcb9a249f16b5010081555186ef83c5c6fceda Status: Downloaded newer image for postgres:9.5 Pulling gogs (gogs/gogs:latest)... latest: Pulling from gogs/gogs e95f33c60a64: Pull complete 6629f7b66676: Pull complete 95248f7b0231: Pull complete 1a4b11e4c1b9: Pull complete 1613ead921c8: Pull complete 37949e6160d8: Pull complete 38df9b14eb06: Pull complete 93d905b51231: Pull complete Digest: sha256:7607fee4a463fdda91e7126fcc8504e7d972c4b80e79b2102c0de7175c1d3b81 Status: Downloaded newer image for gogs/gogs:latest Pulling drone-server (drone/drone:0.8)... 0.8: Pulling from drone/drone 297640bfa2ef: Pull complete 6d34061bcde3: Pull complete Digest: sha256:6056575462036f2579d17885532e5152e5478cf1c495ef183d2381a7e0cd8668 Status: Downloaded newer image for drone/drone:0.8 Pulling drone-agent (drone/agent:0.8)... 0.8: Pulling from drone/agent 297640bfa2ef: Already exists bd501232d024: Pull complete Digest: sha256:a46338f11e89afb1239331d8e4c22f3e44c867e1c81488314833e2d329d9ff40 Status: Downloaded newer image for drone/agent:0.8 Creating root_postgres_1 ... done Creating root_gogs_1 ... done Creating root_drone-server_1 ... done Creating root_drone-agent_1 ... done
1
$ cat docker-compose.yml version: '2' services: postgres: image: postgres:9.5 restart: always environment: - "POSTGRES_USER=gogsuser" - "POSTGRES_PASSWORD=gogspassword" - "POSTGRES_DB=gogs" volumes: - "db-data:/var/lib/postgresql/data" gogs: image: gogs/gogs:latest restart: always ports: - "10022:22" - "3000:3000" links: - postgres environment: - "RUN_CROND=true" volumes: - "gogs-data:/data" depends_on: - postgres drone-server: image: drone/drone:0.8 ports: - 80:8000 - 9000 volumes: - /var/lib/drone:/var/lib/drone/ restart: always links: - gogs environment: - DRONE_OPEN=true - DRONE_GOGS=true - DRONE_GOGS_URL=http://gogs:3000 - DRONE_HOST=${DRONE_HOST} - DRONE_SECRET=SOME_RANDOM_STRING drone-agent: image: drone/agent:0.8 command: agent restart: always depends_on: - drone-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_SERVER=drone-server:9000 - DRONE_SECRET=SOME_RANDOM_STRING volumes: db-data: driver: local gogs-data: driver: local $

访问:https://2886795474-3000-frugo04.environments.katacoda.com
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
创建一个项目
在这里插入图片描述登录
在这里插入图片描述将项目打开
在这里插入图片描述
启动私有Docker Registry

1
$ docker run -p 5000:5000 -d registry:2 Unable to find image 'registry:2' locally 2: Pulling from library/registry ddad3d7c1e96: Pull complete 6eda6749503f: Pull complete 363ab70c2143: Pull complete 5b94580856e6: Pull complete 12008541203a: Pull complete Digest: sha256:bac2d7050dc4826516650267fe7dc6627e9e11ad653daca0641437abdf18df27 Status: Downloaded newer image for registry:2 d89123f5355546675ff654d285009fa2856f6dfe22252698e2de88f73017f122 $

将包含Dockerfile的项目下载下来

1
$ git clone https://github.com/katacoda/golang-http-server && cd golang-http-server Cloning into 'golang-http-server'... remote: Enumerating objects: 82, done. remote: Counting objects: 100% (4/4), done. remote: Compressing objects: 100% (4/4), done. remote: Total 82 (delta 0), reused 1 (delta 0), pack-reused 78 Unpacking objects: 100% (82/82), done. Checking connectivity... done. $

修改仓库地址并推送上去

1
$ cat .drone.yml pipeline: build: image: plugins/docker repo: docker-http-server tags: - 1.0.0 - 1.0 - latest $ sed -i 's|docker-http-server|2886795292-5000-elsy02.environments.katacoda.com/test/docker-http-server|g' .drone.yml $ git remote add gogs http://test:test@2886795292-3000-elsy02.environments.katacoda.com/test/docker-http-server.git && \ > git commit -am 'Updated Repo Path' && \ > git push gogs master [master fd414fe] Updated Repo Path 1 file changed, 1 insertion(+), 1 deletion(-) Counting objects: 40, done. Delta compression using up to 2 threads. Compressing objects: 100% (38/38), done. Writing objects: 100% (40/40), 1.60 MiB | 0 bytes/s, done. Total 40 (delta 15), reused 0 (delta 0) To http://test:test@2886795292-3000-elsy02.environments.katacoda.com/test/docker-http-server.git * [new branch] master -> master $

这个push会触发Drone构建,查看流水线状态
在这里插入图片描述

1
$ curl 2886795292-5000-elsy02.environments.katacoda.com/v2/_catalog {"repositories":["test/docker-http-server"]} $ curl 2886795292-5000-elsy02.environments.katacoda.com/v2/test/docker-http-server/tags/list {"name":"test/docker-http-server","tags":["latest","1.0.0","1"]} $

clone与build完毕,自动将构建好的镜像上传到Private Docker Registry里
在这里插入图片描述

运行容器

1
$ docker run -d -p 8080:80 2886795292-5000-elsy02.environments.katacoda.com/test/docker-http-server Unable to find image '2886795292-5000-elsy02.environments.katacoda.com/test/docker-http-server:latest' locally latest: Pulling from test/docker-http-server d1426d011624: Pull complete dff425fdcaca: Pull complete ac2d6c278079: Pull complete 1260ddb729fc: Pull complete 229cfa4694f2: Pull complete c2bc20ef576d: Pull complete 9f99ecd91c16: Pull complete 1197308fa1ab: Pull complete 465ddf26975a: Pull complete 8fb177b04b86: Pull complete Digest: sha256:8027befe619d5501e2fb23126beb502591b73304cb494000d022e3b7b397b6b1 Status: Downloaded newer image for 2886795292-5000-elsy02.environments.katacoda.com/test/docker-http-server:latest 017b9c2e647160d147f3d4f7fa9c5ac1c937d1aa7cf8d0050412281f39cbea5c $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 017b9c2e6471 2886795292-5000-elsy02.environments.katacoda.com/test/docker-http-server "/app/main" 16 seconds ago Up 14 seconds 0.0.0.0:8080->80/tcp sharp_elbakyan 7cf2383aea37 registry:2 "/entrypoint.sh /etc…" 5 minutes ago Up 4 minutes 0.0.0.0:5000->5000/tcp quirky_turing c7e0fb7d00d1 drone/agent:0.8 "/bin/drone-agent ag…" 9 minutes ago Up 9 minutes (healthy) 3000/tcp root_drone-agent_1 2d9d411a425b drone/drone:0.8 "/bin/drone-server" 9 minutes ago Up 9 minutes 80/tcp, 443/tcp, 0.0.0.0:80->8000/tcp, 0.0.0.0:32768->9000/tcp root_drone-server_1 3d61b6fbb34e gogs/gogs:latest "/app/gogs/docker/st…" 9 minutes ago Up 9 minutes 0.0.0.0:3000->3000/tcp, 0.0.0.0:10022->22/tcp root_gogs_1 1a5723f2a863 postgres:9.5 "docker-entrypoint.s…" 9 minutes ago Up 9 minutes 5432/tcp root_postgres_1 $ curl localhost:8080 <h1>This request was processed by host: 017b9c2e6471</h1> $

概述

  领域驱动设计也就是3D(Domain-Driven Design)已经有了10年的历史,我相信很多人或多或少都听说过这个名词,但是有多少人真正懂得如何去运用它,或者把它运用好呢?于是有人说,DDD和TDD这些玩意是一些形而上的东西,只是一茶余饭后的谈资,又或是放到简历上提升逼格而已。前面这句话我写完之后犹豫了,犹豫要不要把它删掉,因为它让我看起来像个喷子,我确实感到不解,为什么别人10年前创造总结出来的东西,我们在10年之后对它的理解还处于这么低的一个层次。开篇就说远了,我也是最近才开始认真学习领域驱动设计,并且得到了园子里面netfocus,刘标才田园里的蟋蟀的帮助,在此再次表示感谢。希望能和大家一起把DDD普及下去。

  我们之前有一个关于领域驱动设计的讨论,另外dax.net也有一个关于领域驱动设计的系列写得不错,有兴趣的同学可以看看。本文会以一个初学者的角度来讲解DDD,让我们一切从零开始,我相信你跟我一样也会爱上它的。

  本篇主要讨论一下为什么我们要用DDD,它能够为我们带来什么?

领域驱动系列

  初探领域驱动设计(1)为复杂业务而生
  初探领域驱动设计(2)EF 和 Repository
  初探领域驱动设计(3)写好单元测试
  ……

目录

  当我们学习一些设计模式或者框架的时候,总有人会站出来和你说“这些都没有用,只要能实现功能就行了。” 在这里并非针对某个人,实际上我认为他们说的是对的,在资源有限的情况下,我们为了完成项目的交付,这是我们最好的选择。但是别忘了,欠下的债总是要还的,以实现功能为导向的项目务必会造成维护性的大大降低,如果只是一个临时随便用用的东西倒是可以一试,但如果是要长期进行更新的产品,那后期就会拖该产品的后腿。

  我们团队现在维护着一个有着20多年历史的产品,该产品是一个酒店、餐饮行业的POS系统,在美国和亚太地区都占有着比较大的市场份额。该产品从C,C++,VB6一路更新,直到现在的C#,但是很可惜不是整体替换,而是局部的,所以现在项目里面这4种代码全都有。可能你会觉得这玩的是混搭,是潮流,但事实是,一旦产品上线之后,会有很多的新功能,老bug等在那里,再加上“重市场轻技术”的高层在那里制订战略,你压根就没有时间或者没有多少时间去重构。日积月累,等着你的就是每一次改代码都如履薄冰,一不小心就因为改一个bug而整出好几个新bug出来,前不久我们为了新版本的发布就停下所有开发的任务,大家集体花了1个月的时间去做回归测试了。因为前期发布新版本之后bug太多,所以这次老大们都不敢轻易发布了。:)

  这是我们血的教训,如果你前期只顾开发功能,最后就会让你很难再开发新功能。所以真诚的希望大家不要再片面的说“只要实现功能就可以了!”,软件开发的领域这么大,我们没有必要把自己局限在某一个框框里面。对于大型系统来说,我们要学习的地方还有很多:

  • 组织良好、可阅读性高的代码可以让其它开发人员很容易的开始去修改代码。
  • 低耦合,高内聚 - 适合运用设计模式以及原则来设计一些好的框架可以降低修改代码引发新bug的风险。
  • 良好的单元测试以及集成测试可以及时的帮助我们检测新增或修改的代码是否会破坏原有的逻辑。
  • 自动化测试绝对是省时省力的好帮手,也是项目质量的保证。
  • 持续集成可以帮助我们更快速安全的进行迭代。

  上面说了这么多也没有提到DDD,那么为什么它能够在构建复杂系统的时候有优势呢?我们可以从以下几个点去思考:

  • 从设计阶段出发,站在业务的角度思考问题
  • 厘清业务主次
  • 独立领域业务层,打通开发和测试阶段
  • 干净的代码

  除了DDD,现在还流行另外一个词汇TDD。但是不知道大家有没有注意到DDD(Domain-Driven Design)中的D代表着设计,而TDD(Test-Driven Development)中的D代表着开发,你有没有曾几何时把领域驱动设计说成领域驱动开发呢?当然我们确实是可以根据领域驱动来开发,但是DDD被设计出来的完美初衷却是设计。TDD强调的已经是开发了,要求开发人员先写单元测试然后再通过不断的迭代重构让单元测试通过,以此来实现功能。这样做的好处是强迫让开发人员清楚正确的理解需求,要知道这年头没有正确理解需求就开始写代码的程序员大有人在,并且我不认为需求就是业务,需求已经是将本来的业务理解之后,转化为了通过计算机可以实现的一些功能定义,通常是业务分析师或者项目经理会去完成这个工作。而DDD中的D(领域)更像是本来的业务,所以在领域驱动设计的时候,开发人员或者架构师直接与领域专家(或者说客户)进行沟通来建模,这些业务模型也是以后开发人员进行设计和实现的依据。

  领域模型被当作开发人员之间,开发人员与领域专家之间沟通的桥梁,这样可以闭免开发人员用错误的方式去实现功能。实际上很多优秀的开发人员,都会很自然的将现实世界中的问题进行抽象,然后用计算机的语言表示出来,我们称之为面向对象。但是由于缺少亲临其境的体验,往往会离真实的业务模型有一些距离。

  我们举一个例子来说明一下这个问题,假如我们要开发一个电子商务的网站,这个需求已经非常清楚了,现在那么多的电子商务网站直接照抄一个就可以了。现在我们来做一个下单的功能,来看看怎么去实现 。

  作为一个高级程序员,我们得用面向对象的方式去开发,先建类。于是我们有了用户,订单,订单项的类,用户创建订单然后往订单里面添加商品,添加订单项的时候为了方便,我们只需要传入产品ID和数量就可以了,于是Order类有一个AddItem的方法。

  作为一个高级程序员,一看这图感觉很完美,有木有? 好,下面开始实现AddItem方法。

  Order里面是一个OrderItem的集合,而这个AddItem的方法接收的是productId,我去哪里搞个Product对象给你?我不可能在这个实体里面直接去查数据库吧?本来是冲着这个技术点想咨询一下大家,后来在小组里面讨论了一下,我恍然大悟,上面的实体就是我从代码的层面去思考想出来的,下单嘛,当然是用户,订单和订单项喽。可是只要去网上买过东西都知道,用户是不会直接往订单里面加东西的,而是先把商品加入购物车,然后再通过“结算”一次性就根据购物车生成了一张订单,压根没有往订单里面添加订单项的行为。这才是真正的用户行为(领域逻辑)所以后来,我们的实体变成这样了:

  所以业务是这样的:
    未注册用户也可以将商品添加到购物车中,但是不能下订单。
  并且购物车中的商品不能保存起来,用户离开这个网站(一般是关掉浏览器),购物车中的商品就会消失。
  注册用户购物车中的商品可以长期永久保存,通过购物车的“结算功能”,将购物车中选中的商品转化为订单。
  所以购物车,应该在用户注册的时候就应该创建好,对应我们上面的User实体中的CreatShoppingCart()方法。下面我们先来简单实现一下注册的代码。

//User领域实体代码

1 namespace RepositoryAndEf.Domain 2 {
3 public class User : BaseEntity 4 {
5 public string Name { get; set; }
6 public string Email { get; set; }
7 public string Password { get; set; }
8 public Guid ShoppingCartId { get; set; }
9 public virtual ShoppingCart ShoppingCart { get; set; } 10 public virtual ICollection Orders { get; set; } 11
12 public void CreateShoppingCart() 13 { 14 ShoppingCart = new ShoppingCart 15 { 16 Id = Guid.NewGuid(), 17 Customer = this, 18 CustomerId = Id, 19 }; 20
21 ShoppingCartId = ShoppingCart.Id; 22 } 23 } 24 }

View Code

//领域层 UserService.cs代码

1 namespace RepositoryAndEf.Domain 2 {
3 public class UserService 4 {
5 private IRepository _userRepository; 6
7 public UserService(IRepository userRepsoitory) 8 {
9 _userRepository = userRepsoitory; 10 } 11
12 public virtual User Register(string email, string name, string password) 13 { 14 var user = new User 15 { 16 Id = Guid.NewGuid(), 17 Email = email, 18 Name = name, 19 Password = password 20 }; 21
22 user.CreateShoppingCart(); 23 _userRepository.Insert(user); 24 return user; 25 } 26 } 27 }

View Code

//应用层 UserService.cs代码

1 namespace RepositoryAndEf.Service 2 {
3 public class UserService : IUserService 4 {
5 protected Domain.UserService DomainuUserService 6 {
7 get
8 {
9 return EngineContext.Current.Resolve<Domain.UserService>(); 10 } 11 } 12
13 public User Register(string email, string name, string password) 14 { 15 var user = DomainuUserService.Register(email, name, password); 16 return user; 17 } 18 } 19 }

View Code

  上面是我们一次建模的过程,是一个将业务转变成代码,将现实世界抽象成软件世界的过程。我们需要画出模型不断的与业务人员(领域专家)去沟通,然后不断的重构去完善我们的模型,以至于这个模型能最准确的反映真实的业务。这是在最开始的设计阶段,是需求沟通阶段就需要做的工作,并且会一直贯穿我们后面的开发甚至维护阶段,没有人可以一开始就把领域模型建的100%准确,需求是复杂的,并且需求还是随时变化的,所以模型也会一直发生改变。它将作为开发人员与业务人员、测试人员以及开发人员自己之间沟通的桥梁。而DDD与其它方法论的区别之处就在于,它还提供了一整套的体系来保证后续对领域模型的重构不会让系统变得四分五裂,比如架构分层,仓储,依懒注入等等,我们后面再慢慢探讨。

   在DDD中,领域模型分为三种:

  1. 实体
  2. 值对象
  3. 领域服务

区分实体、值对象和领域服务

  我们不打算去解释以上的概念,我相信只要你搜索一下就可以得到很全面准确的答案。但是重要的是我们一定要理解3者之间的区别,什么时候是实体,什么时候是值对象,又是什么时候我们该用领域服务呢?我想这是刚接触DDD的人都难免会有些纠结的地方吧,在这里就强调一下。

  实体相对于值对象而言拥有“标识”的概念,标识可以让我们持续性的跟踪实体。标识和数据库里面的“主键”是不一样的概念,主键是技术上的概念,但是标识是业务上的概念。

  在我们上面的例子中用户ID是标识,我们用它来持续性的跟踪我们的用户。订单ID是标识,我们用它来持续性的跟踪订单,同时我们的用户和订单都是有着不同的状态。但是对于用户的地址来说,我们用什么来做标识呢?在电子商务网站这样的业务里面,我们不需要去持续的跟踪这个地址信息,它在我们的系统里面也不会有着像订单从“创建”、“已付款”、“已发货”、“已收货”等这样的状态,所以地址信息的我们系统中就是一个值对象。

  但是我们如果换了一个系统,比如说死慢的长城宽带,他们把地址作为跟踪对象。同一个地址,谁都可以去注册,但是同一个时间只允许一个人去注册,那么这个地址对于长城宽带来说就去要去持续性的跟踪,有“开户”,“销户”的状态。那么地址信息对于长城宽带来说就是一个实体。

  解决完实体和值对象,领域服务就好说了,一些重要的领域操作,既不属于实体也不属于值对象,那就可以把它放到服务中了。比如说我们上面的领域服务UserService里面的注册操作,注册这个操作可以说就是将这个用户保存到我们的系统中。在注册之间,这个用户是不存在的,我们又怎么能把注册这个操作放到User实体中去呢?所以把它放到领域服务中成了我们最好的选择。

  即使是这样,哪些操作应该放到领域服务中对于很多初学者来说还是一件比较难选择的问题。也许只有慢慢的对业务越来越了解,对DDD应用的越来越熟,我们就会少一点纠结。

  在上面的模型中,我们有很多关系的存在:用户-购物车(1对1),用户-订单-订单项-产品(1对多,1对1),购物车-购物车项-产品等。在DDD中,我们把这样多个模型用关联串起来组成一个**聚合(aggregation)**。

  在我们的模型中,购物车-购物车项是一个聚合,订单-订单项是一个聚合我们通常需要保护这些聚合的一致性,比如说我们把一个订单删掉了,那么这个订单的订单项也需要一起删除,否则他们存在也没有任何的意义。以前我们还会用到触发器,但是大家都知道这个东西维护起来比较麻烦,写起来也不方便等,所以后来大家都是在代码中来控制。但是一直没有一个好的约束说我们如何去更好的控制这些一致性,代码一直都很散乱,直到DDD,我们有了聚合和聚合根的概念,“我们通过为每一个聚合选择一个根,并通过根来控制所有对边界内的对象的访问。外部对象只能持有根的引用;由于根控制了访问,因此我们无法绕过它去修改内部元素。我们后面还会说到只能为根来建立Repository,这也是为了确保我们这里面讲的数据的一致性。

  在我们上面的聚合中,只能通过购物车实体来操作购物车项,而不能你自己写一个保存的方法直接就把购物车项给保存到数据库中去了。这就是聚合和聚合根起到的作用。我们来看一下我们购物车实体的代码:

1 namespace RepositoryAndEf.Domain 2 {
3 public class ShoppingCart : BaseEntity 4 {
5 public ShoppingCart() 6 {
7 Items = new List();
8 }
9
10 #region Properties
11
12 public Guid CustomerId { get; set; } 13 public virtual User Customer { get; set; } 14 public virtual ICollection Items { get; set; } 15
16 #endregion
17
18 #region Methods
19 public void AddItem(Product product, int quantity) 20 { 21 // 如果该产品ID已经存在于购物车中,我们直接更改数量即可
22 var repetitiveCartItem = Items.FirstOrDefault( 23 i => i.ProductId == product.Id); 24
25 if (repetitiveCartItem != null) 26 { 27 repetitiveCartItem.Quantity += quantity; 28 return; 29 } 30
31 Items.Add(new ShoppingCartItem 32 { 33 Product = product, 34 ProductId = product.Id, 35 Quantity = quantity, 36 }); 37 } 38
39 // 更改购物车数量
40 public void ChangeProductQuantity(Guid productId, int newQuantity) 41 { 42 var items = Items as ICollection; 43 var existingCartItem = items.FirstOrDefault( 44 i => i.ProductId == productId); 45
46 if (existingCartItem == null) 47 { 48 throw new InvalidOperationException( 49 “Cannot find the product in shopping cart”); 50 } 51 existingCartItem.Quantity = newQuantity; 52 } 53
54 // 从购物车中移除该产品
55 public void RemoveItem(Guid productId) 56 { 57 var items = Items as ICollection; 58 var existingCartItem = items.FirstOrDefault( 59 i => i.ProductId == productId); 60
61 if (existingCartItem == null) 62 { 63 throw new InvalidOperationException( 64 “Cannot find the product in shopping cart”); 65 } 66
67 items.Remove(existingCartItem); 68 } 69 #endregion
70 } 71 }

View Code

  大家可以看到我们购物车实体的逻辑很清晰,因为我们很明确购物车拥有哪些操作。当然还有另一种做法即把这些操作都放到用户实体中去,因为最终其实是用户做的这些操作。那我们的聚合就变成了用户-购物车-购物车项,这样也没有什么不可以,反而更符合真实的场景。但是会导致我们的聚合过庞大,也就是说我必须要先有用户实体才能进行操作,用户用户可能会绑上很多的东西:购物车、订单、地址等等。在现在都是ajax来操作的大型网站中,我们需要在服务端把这个用户请求加载出来再执行添加购物车的操作呢?还是可以直接加载购物车实体来操作呢?这就是一个粒度的问题,不同的问题和场景,大家可以区别来对待。总之聚合是可以根据业务或者一些特定需求来做出调整的。比如说购物车-购物车项-产品,这也是一个聚合,但是由于产品的特殊性,我们可以把产品也作为一个聚合根来单独进行访问。

  我们来看一下应用层ShoppingCartService的代码:

1 public class ShoppingCartService : IShoppingCartService 2 {
3 private IRepository _shoppingCartRepository; 4 private IRepository _productRepository; 5
6 public ShoppingCartService(IRepository shoppingCartRepository, 7 IRepository productRepository) 8 {
9 _shoppingCartRepository = shoppingCartRepository; 10 _productRepository = productRepository; 11 } 12
13 public ShoppingCart AddToCart(Guid cartId, Guid productId, int quantity) 14 { 15 var cart = _shoppingCartRepository.GetById(cartId); 16 var product = _productRepository.GetById(productId); 17 cart.AddItem(product, quantity); 18
19 _shoppingCartRepository.Update(cart); 20 return cart; 21 } 22
23 }

View Code

   此应用层代码一出,大家就会发现,这代码太简洁了,有木有?因为所有的逻辑、业务都被放到领域实体那里面去处理了。即使我们业务逻辑改变了,或者我们需要重构了,它们都在领域实体那里面,改那里就好了。接下来的问题是,如何确保安全,正确的一次又一次的对领域实体进行重构呢?毕竟它也是各种关联,各种依懒呀?您请接着往下看我们的单元测试环节。

  讲到这里,请允许我从网上盗一张图,当然这张图早就已经是被引用过无数次了,它就是DDD中使用的分层结构。

  关于这个分层,每一层是干什么的,具体怎么玩,大家可以看一下dax的这一篇文章讲解的很清楚。总之,我们的领域模型以及相关的类比如工厂等会被独立成为一层来与应用层和基础设计层交互。

   领域层是独立的,首先它是应用层的下层,所以肯定不会有对应用层的依懒,但是领域有一些模型或者服务少不了是要与数据库打交道的,比如说我们在注册用户的时候需要去验证当前的邮箱是不是已经被占用了。而这一类操作都是属于基础设施层做的事情,包含像一些数据库操作,日志,缓存等等。那么我们如何避免领域层对基础设施层的依懒呢?感谢面向对象设计 - 面向接口编程,只不过这里面的场景特别有代表性,它是一个非常常见的问题,于是它成为了一个模式:仓储(Repository)。

1 namespace RepositoryAndEf.Core.Data 2 {
3 public partial interface IRepository where T : BaseEntity 4 {
5 T GetById(object id); 6
7 IEnumerable Get( 8 Expression<Func<T, Boolean>> predicate); 9
10 bool Insert(T entity); 11 bool Update(T entity); 12 bool Delete(T entity); 13 } 14 }

View Code

  一般情况下,我们会把仓储的接口放到领域层,或者也可以再建一个Core层来作个项目最下面的那一层提供一些最公共的组件部分。关于仓储的代码,大家在上面领域服务UserService中的注册代码中就已经见到过了。可能需要注意的是,Repository用来将数据库与其它的业务和技术分离,所以我们在领域层中使用它,还在应用层中使用它。

  Repository让我们专注于模型,不用去考虑持久化的问题。更为重要的一点是,因为它是接口,所以我们可以很方便的替代它,或者模拟一个实现来对我们的领域模型进行单元测试。下面是我们实现的MockRepository的代码:

1 public class MockRepository: IRepository where T : BaseEntity 2 {
3 private List _list = new List();
4
5 public T GetById(Guid id) 6 {
7 return _list.FirstOrDefault(e => e.Id == id); 8 }
9
10 public IEnumerable Get(Expression<Func<T, bool>> predicate) 11 { 12 return _list.Where(predicate.Compile()); 13 } 14
15 public bool Insert(T entity) 16 { 17 if (GetById(entity.Id) != null) 18 { 19 throw new InvalidCastException(“The id has already existed”); 20 } 21
22 _list.Add(entity); 23 return true; 24 } 25
26 public bool Update(T entity) 27 { 28 var existingEntity = GetById(entity.Id); 29 if (existingEntity == null) 30 { 31 throw new InvalidCastException(“Cannot find the entity.”); 32 } 33
34 existingEntity = entity; 35 return true; 36 } 37
38 public bool Delete(T entity) 39 { 40 var existingEntity = GetById(entity.Id); 41 if (existingEntity == null) 42 { 43 throw new InvalidCastException(“Cannot find the entity.”); 44 } 45
46 _list.Remove(entity); 47 return true; 48 }

View Code

  下面我们给我们User领域实体的注册方法加一个检查Email是否存在的逻辑。

1 public virtual User Register(string email, string name, string password) 2 {
3 if (_userRepository.Get().Any(u => u.Email == email)) 4 {
5 throw new ArgumentException(“email has already existed”);
6 }
7
8 var user = new User 9 { 10 Id = Guid.NewGuid(), 11 Email = email, 12 Name = name, 13 Password = password 14 }; 15
16 user.CreateShoppingCart(); 17 _userRepository.Insert(user); 18 return user; 19 }

View Code

  在我们真实的Repository出来之前,不管我们是打算是EF,还是NHibernate,我们现在只要对这个Mock的Repository来编程或者进行单元测试就可以了。

//UserService领域服务在单元测试

1 public class UserServiceTests 2 {
3 private IRepository _userRepository = new MockRepository();
4
5 [Fact]
6 public void RegisterUser_ExpectedParameters_Success() 7 {
8 var userService = new UserService(_userRepository); 9 var registeredUser = userService.Register( 10 “hellojesseliu@outlook.com“, 11 “Jesse”, 12 “Jesse”); 13
14 var userFromRepository = _userRepository.GetById(registeredUser.Id); 15
16 userFromRepository.Should().NotBe(null); 17 userFromRepository.Email.Should().Be(“hellojesseliu@outlook.com“); 18 userFromRepository.Name.Should().Be(“Jesse”); 19 userFromRepository.Password.Should().Be(“Jesse”); 20 } 21
22 [Fact] 23 public void RegisterUser_ExistedEmail_ThrowException() 24 { 25 var userService = new UserService(_userRepository); 26 var registeredUser = userService.Register( 27 “hellojesseliu@outlook.com“, 28 “Jesse”, 29 “Jesse”); 30
31 var userFromRepository = _userRepository.GetById(registeredUser.Id); 32 userFromRepository.Should().NotBe(null); 33
34 Action action = () => userService.Register( 35 “hellojesseliu@outlook.com“, 36 “Jesse_01”, 37 “Jesse”); 38 action.ShouldThrow(); 39 } 40 }

View Code

   我们用的XUnit.net作单元测试框架,同时用了Fluent Assertions。

  结果很漂亮,有木有?有了单元测试来为我们的领域模型保驾护航,我们就可以安全的进行重构了。

  经常有人说代码是一件艺术,码农都是艺术家。我很喜欢这句话,如果你也认同,那就请像对待艺术品一样对待我们的代码,精心的打磨它。并且你不一定要非常的有经验才可以干这件事情;

  如果你刚入行,那至少保证一代码可读性好(好的命名,代码逻辑清晰等);
  再往上一点,你要能够更好的组织代码(类,函数);
  等到你也成为专家了,那就开始考虑一些重用性,可扩展性,可维护性,可测试性的这些比较范的东西了;
  而最后就上升到架构层面,考虑系统各个组件之间通讯,分层,等等。最后你就成为码神了。

  DDD里面引入的一些思路包括分层、依懒注入、仓储等,可以给我们一些指导,大家从上面的代码也可以看出这些代码组织的很好,逻辑也不会散乱的到处都是。当然这个项目代码量有限,说服力是有限的,后面我们还会尝试去加入应用层的代码。代码已经放到CodePlex上去了:http://repositoryandef.codeplex.com
欢迎大家Follow。注意代码还没有写完,只是一个初级版本,我们后面会慢慢完善。这个项目会使用EF来作业ORM框架,Autofac作依懒注入容器,用Xunit作单元测试框架的同时引入了Fluent Assertions。

   本文主要介绍了DDD的一些基础概念:

  • 领域模型:领域实体、领域服务以及值对象;建模一定要从真实的领域业务出发,多与领域专家进行沟通来完善模型。
  • 聚合与聚合根:它的主要作用是用来确保各种关系下的实体的数据一致性;但是确认聚合根这个过程,实际上也是对业务的梳理过程。
  • 架构分层: 每一层都职责清楚;依懒于接口来降低耦合。
  • 封装和测试: 所有的业务都放到领域层,同时对领域层进行单元测试来确保最核心的逻辑不会遭到破坏。

  个人感觉没有必要太强调Repository的概念,从领域实体的生命周期(创建-持久化到数据库-销毁-从数据库重建)你会发现其实这个过程很普遍,并不是只有DDD才有的。所以我认为Repository主要是将数据访问功能给隔离开,避免领域实体对基础设施层的依懒。那它和三层有什么区别? BLL 引用DAL不也是依懒于接口么?给我的感觉是,DDD的领域实体持久化这一块就是三层里面的思路。这可能是在学习DDD初期的想法,因为真实的大型项目中是不会直接把领域实体给持久化的,那个叫DTO,于是Repository<>里面放的就不是我们的领域实体了,而是将领域实体转换成对应的DTO。 

  是否一定要使用DTO呢?领域实体和DTO互相转换,最后到了表现层DTO还要和ViewModel转换,会不会带来复杂性和性能上的损失?Repository和EF还有Unit Of Work怎么来协调?抱怨写单元测试么?怎么样让写单元测试不变成只是走过场而已? 这些问题留给我们后面再解决吧。

明确了请求方式,提供对应后台所需参数,请求url链接可以得到后台的响应数据
url : 返回数据的url
https://api.map.baidu.com/place/search
请求方式:
get,post,put,patch….

请求参数:json或xml格式的key-value类型数据

  • ak:6E823f587c95f0148c19993539b99295
  • region:上海
  • query:肯德基
  • output:json

响应结果:
返回json或xml格式的key-value类型数据

  • 上方请求参数的output参数值决定了响应数据的格式

参照某种规则(规范)书写url链接,同时根据规则制定请求方式,请求数据与响应结果

提供给前后台开发人员与测试人员查看

YAPI平台

专门写接口文档的YAPI平台 http://yapi.demo.qunar.com/

YApi是去哪儿网的前端技术中心的一个开源可视化接口管理平台

img

  • 创建接口项目

img

  • 创建接口

img

  • 编写接口

img

img

img

接口测试工具:Postman

Postman是一款接口调试工具,是一款免费的可视化软件,同时支持各种操作系统平台,是测试接口的首选工具。

Postman可以直接从官网:https://www.getpostman.com/downloads/下载获得,然后进行傻瓜式安装。

  • 工作面板

img

  • 简易的get请求

img

  • 简易的post请求

img

  • 案例:请求百度地图接口

img

webapi接口规范:restful

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

推荐阅读 阮一峰 理解RESTful架构

推荐阅读 阮一峰 RESTful设计指南

API与用户的通信协议

总是使用HTTPs协议

域名

用api关键字来标识接口url

https://api.example.com

https://example.org/api/

注:看到api字眼,就代表该请求url链接是完成前后台数据交互的

版本

\1. 将版本信息放在URL中,如:

https://api.example.com/v1/

https://api.example.com/v2/

v1,v2代表不同数据版本的提现,前提是一种数据资源有多个版本

\2. 将版本信息放在请求头中。

url路径

视网络上任何东西都是资源,均使用名词表示(一般为复数形式)

https://api.example.com/v1/zoos

https://api.example.com/v1/animals

https://api.example.com/v1/employees

在url链接中奖励不要出现操作资源的动词

错误示范:https://api.baidu.com/delete-user

特殊的接口可以出现动词,因为这些接口一般没有一个明确的资源,或是动词就是接口的核心含义

method请求方式

GET :从服务器取出资源(一项或多项)

POST :在服务器新建一个资源

PUT :在服务器更新资源(客户端提供改变后的完整资源)

PATCH :在服务器更新资源(客户端提供改变的属性)

DELETE :从服务器删除资源

过滤

通过在url上传参的形式传递搜索条件

https://api.example.com/v1/zoos?limit=10:指定返回记录的数量

https://api.example.com/v1/zoos?offset=10:指定返回记录的开始位置

https://api.example.com/v1/zoos?page=2&per_page=100:指定第几页,以及每页的记录数

https://api.example.com/v1/zoos?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序

https://api.example.com/v1/zoos?animal_type_id=1:指定筛选条件

状态码

错误处理

状态码是4xx时,应返回错误信息,error当做key。    

返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范

Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

__EOF__

(⊙o⊙)?原来边缘计算是这么开发的!_cloudcore edgecore 通信-CSDN博客

Excerpt

文章浏览阅读3.4k次,点赞3次,收藏18次。KubeEdge是华为开源出来的边缘计算组件,它是基于kubernetes之上将容器化应用的编排能力拓展到边缘主机或边缘设备,在云端和边缘端提供网络通信,应用部署、元数据同步等功能。同时支持MQTT协议,允许开发者在边缘端自定义接入边缘设备。_cloudcore edgecore 通信


边缘计算

1. KubeEdge简介

KubeEdge是华为开源出来的边缘计算组件,它是基于kubernetes之上将容器化应用的编排能力拓展到边缘主机或边缘设备,在云端和边缘端提供网络通信,应用部署、元数据同步等功能。同时支持MQTT协议,允许开发者在边缘端自定义接入边缘设备。

为什么要用KubeEdge而不直接使用K8s?

KubeEdge是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的。

2. 功能说明

  • 边缘计算:提供边缘节点自治能力,边缘节点数据处理能力。
  • 便捷部署:开发者可以开发http或mqtt协议的应用,运行在云端和边缘端。
  • k8s原生支持:可以通过k8s管理和监控边缘设备和边缘节点。
  • 丰富的应用类型:可以在边缘端部署机器学习、图片识别、事件处理等应用。

3. 架构说明

3.1 架构图

kubeedge分为两个可执行程序,cloudcore和edgecore。

在这里插入图片描述

3.2. 云端

  • CloudHub:一个web socket服务器,负责监听云端的更新、缓存及向EdgeHub发送消息。
  • EdgeController:一个扩展的k8s控制器,负责管理边缘节点和pod元数据,同步边缘节点的数据,是k8s-apiserverEdgeCore的通信桥梁。
  • DeviceController:一个扩展的k8s控制器,负责管理节点设备,同步云端和边缘端的设备元数据和状态。

3.3. 边缘端

  • EdgeHub:一个web socket客户端,负责云端与边缘端的信息交互,其中包括将云端的资源变更同步到边缘端及边缘端的状态变化同步到云端。它充当边缘与云之间的通信链接。

  • Edged:运行在边缘节点,管理节点生命周期。可以帮助用户在边缘节点上部署容器化的工作负载或应用程序。 这些工作负载可以执行任何操作,从简单的遥测数据操作到分析或ML推理等。使用kubectl云端的命令行界面,用户可以发出命令来启动工作负载。

  • EventBus:一个MQTT客户端,充当用于发送/接收有关mqtt主题的消息的接口。与MQTT服务端交互,提供发布/订阅的能力。

  • ServiceBus:一个HTTP客户端,与HTTP服务端交互。为云组件提供HTTP客户端功能,以访问在边缘运行的HTTP服务器。

  • DeviceTwin:负责存储设备状态并同步设备状态到云端,同时提供应用的接口查询。

  • MetaManager:edgededgehub之间的消息处理器,负责向轻量数据库(SQLite)存储或查询元数据。

4. 部署

4.1环境准备

我们在上篇文章中安装好的k8s基础上再增加1台边缘端节点。

从零开始学习部署K8S:https://blog.csdn.net/shyflea/article/details/119758389

注意:往下操作前要先安装好K8S!!!

主机信息如下,系统均采用Centos7.x。

类型 IP地址 主机名 内存 CPU 部署服务
云端 192.168.159.101 master1 4G 2 k8s、docker、cloudcore
云端 192.168.159.102 node1 2G 1 docker
云端 192.168.159.103 node2 2G 1 docker
边缘端 192.168.159.201 edge1 1G 1 docker、edgecore

4.2 云端部署

云端主要是负责编译 kubeEdge 的相关组件与运行 cloudcore,所以需要准备 golang 环境,以及需要去官方 github 上拉取源码,进行编译。

4.2.1 配置 golang 环境

1
$ cd /home $ wget https://golang.google.cn/dl/go1.14.4.linux-amd64.tar.gz $ tar -zxvf go1.14.4.linux-amd64.tar.gz -C /usr/local

配置 golang 环境变量

1
$ vim /etc/profile 文件末尾追加 # golang env export GOROOT=/usr/local/go export GOPATH=/data/gopath export PATH=$PATH:$GOROOT/bin:$GOPATH/bin $ source /etc/profile # 创建工作目录: $ mkdir -p /data/gopath && cd /data/gopath $ mkdir -p src pkg bin

4.2.2 配置 cloudcore

下载 kubeEdge 源码

1
# 由于从github上下载比较慢,这里使用加速地址 $ cd /home $ git clone https://hub.fastgit.org/dogfei/kubeedge.git $GOPATH/src/github.com/kubeedge/kubeedge

编译 keadm

1
$ cd $GOPATH/src/github.com/kubeedge/kubeedge $ make all WHAT=keadm $ make all WHAT=cloudcore $ yum -y install gcc $ make all WHAT=edgecore

将编译好的二进制文件 copy 到/usr/local/bin 中

1
$ cp _output/local/bin/* /usr/local/bin/

4.2.3 创建cloud节点

创建 cloudcore 节点

1
$ keadm init --advertise-address="192.168.159.101" #IP替换为本机IP

问题1: 连接不上raw.githubusercontent.com

解决方法:编辑vi /etc/hosts,增加

1
151.101.108.133 raw.githubusercontent.com

问题2: 如果添加完host以后,还是

1
> Error: failed to run 'sh -c cd /etc/kubeedge/crds/devices && wget -k > --no-check-certificate --progress=bar:force https://raw.githubusercontent.com/kubeedge/kubeedge/master/build/crds/devices/devices_v1alpha2_device.yaml' > because of error : exit status 4

解决方法:手动下载后上传到相应目录

1
# 上传devices_v1alpha2_devicemodel.yaml 和devices_v1alpha2_device.yaml $ cd /etc/kubeedge/crds/devices # 下载地址:https://github.com/kubeedge/kubeedge/edit/master/build/crds/devices/ # 上传 cluster_objectsync_v1alpha1.yaml objectsync_v1alpha1.yaml $ cd /etc/kubeedge/crds/reliablesyncs # 下载地址:https://github.com/kubeedge/kubeedge/edit/master/build/crds/reliablesyncs
1
# 手动下载kubeedge-v1.7.2-linux-amd64.tar.gz安装包 $ cd /etc/kubeedge/ $ wget -k --no-check-certificate --progress=bar:force https://ghproxy.com/https://github.com/kubeedge/kubeedge/releases/download/v1.7.2/kubeedge-v1.7.2-linux-amd64.tar.gz # 重新启动 $ keadm init --advertise-address="192.168.159.101" # 注意安装过程中提示是否删除kubeedge-v1.7.2-linux-amd64.tar.gz重新下载,要选N

在这里插入图片描述

4.2 Edge 端部署

4.2.1 环境初始化

首先对边缘端edge1进行初始化操作:

关闭防火墙和SELinux

1
$ systemctl stop firewalld && systemctl disable firewalld $ setenforce 0 $ vi /etc/selinux/config SELINUX=disabled

设定/etc/host解析到互通的主机

云端控制节点master1:

1
$ vi /etc/hosts 192.168.159.201 edge1

边缘端节点edge1:

1
$ vi /etc/hosts 192.168.159.201 edge1 192.168.159.101 master1

配置时间同步定时任务

1
# 使用公共NTP网络时间服务器地址:阿里云 * * * * * /sbin/ntpdate time1.aliyun.com > /dev/null 2>&1

临时关闭swap分区

1
$ swapoff -a # 永久关闭 $ sed -ri 's/.*swap.*/#&/' /etc/fstab

4.2.2 安装部署docker

在边缘节点安装docker

1
# 1.设置国内YUM源 $ cd /etc/yum.repos.d/ $ yum install wget $ wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # 2.安装指定的docker版本 $ yum -y install docker-ce-18.09.7-3.el7 docker-ce-cli-18.09.7 # 3.设置docker使用阿里云加速 $ mkdir /etc/docker $ cat > /etc/docker/daemon.json <<EOF { "registry-mirrors": ["https://ig2l319y.mirror.aliyuncs.com"], "exec-opts": ["native.cgroupdriver=cgroupfs"] } EOF # 4.启动后台进程 $ systemctl enable docker && systemctl start docker # 5.查看docker版本 $ docker -v

4.2.3 安装keadm

Edge 端也通过 keadm 进行配置,可以将 cloud 端编译生成的二进制文件 scp 到 Edge 端。

在云端执行如下命令:

1
$ cd $GOPATH/src/github.com/kubeedge/kubeedge $ scp -r _output/local/bin/* root@192.168.159.201:/usr/local/bin/ $ scp /etc/kubeedge/kubeedge-v1.7.2-linux-amd64.tar.gz root@192.168.159.201:/etc/kubeedge/

4.2.4 从云端获取令牌 token

获取的令牌用于边缘节点加入时使用,类似于 k8s 的节点加入集群中

1
$ keadm gettoken # 输出结果 576713eff1f92ea4a48fda83538354b25d1e42190d17bbbc7b36e07980dfae2c.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mjk2MDI3OTJ9.DH78U1CoFiW_v-Qlnscf40b7De7dAy1-Y2bc7FMkxxg

4.2.5 加入边缘节点

在边缘端执行以下命令

1
$ keadm join --cloudcore-ipport=192.168.159.101:10000 --token=576713eff1f92ea4a48fda83538354b25d1e42190d17bbbc7b36e07980dfae2c.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mjk2MDI3OTJ9.DH78U1CoFiW_v-Qlnscf40b7De7dAy1-Y2bc7FMkxxg # 出现以下输出表示安装成功 KubeEdge edgecore is running, For logs visit: journalctl -u edgecore.service -b

4.3 验证

边缘端 edgecore 启动成功后,会与云端的 cloudcore 进行通信,并且边缘端会加入到 k8s 集群中,所以可以使用 kubectl 命令进行验证

1
$ kubectl get nodes NAME STATUS ROLES AGE VERSION edge1 Ready agent,edge 15m v1.18.6-kubeedge-v1.4.0-beta.0.242+8280082f9da41b master1 Ready master 4d8h v1.16.2 node1 Ready node 4d5h v1.16.2 node2 Ready node 4d5h v1.16.2

在控制台也能够看到新加入的边缘节点。
在这里插入图片描述

5. 边缘计算应用Demo

这里拿官方的一个例子,进行测试。

KubeEdge Counter Demo 计数器是一个伪设备,用户无需任何额外的物理设备即可运行此演示。计数器在边缘侧运行,用户可以从云侧在 Web 中对其进行控制,也可以从云侧在 Web 中获得计数器值,原理图如下:
在这里插入图片描述

5.1 准备工作 (云端操作)

下载示例代码:

1
# 使用官方的示例仓库会比较慢,这里使用加速仓库 $ git clone https://hub.fastgit.org/kubeedge/examples.git $GOPATH/src/github.com/kubeedge/examples

创建 device model

1
$ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/crds $ kubectl create -f kubeedge-counter-model.yaml

创建 model

1
#需要根据实际情况修改yaml文件,修改matchExpressions $ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/crds $ vim kubeedge-counter-instance.yaml apiVersion: devices.kubeedge.io/v1alpha2 kind: Device metadata: name: counter labels: description: 'counter' spec: deviceModelRef: name: counter-model nodeSelector: nodeSelectorTerms: - matchExpressions: - key: 'kubernetes.io/hostname' operator: In values: - edge1 status: twins: - propertyName: status desired: metadata: type: string value: 'OFF' reported: metadata: type: string value: '0' # 运行yaml $ kubectl create -f kubeedge-counter-instance.yaml

5.2 部署云端应用

云端应用 web-controller-app 用来控制边缘端的 pi-counter-app 应用,该程序默认监听的端口号为 80,此处修改为 8089,如下所示:

1
$ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/web-controller-app $ vim main.go package main import ( "github.com/astaxie/beego" "github.com/kubeedge/examples/kubeedge-counter-demo/web-controller-app/controller" ) func main() { beego.Router("/", new(controllers.TrackController), "get:Index") beego.Router("/track/control/:trackId", new(controllers.TrackController), "get,post:ControlTrack") beego.Run(":8089") }

构建镜像

1
#注意:构建镜像时,如果源码不在GOPATH对应的路径下,请将源码拷贝到GOPATH对应的路径下,如果开启了go mod请关闭。 $ make all $ make docker #这里会进行构建镜像操作

部署 web-controller-app

1
$ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/crds $ kubectl apply -f kubeedge-web-controller-app.yaml

5.3 部署边缘端应用

修改代码并构建镜像

需要将 Makefile 中的 GOARCH 修改为 amd64 才能运行该容器。

1
$ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/counter-mapper $ vim Makefile .PHONY: all pi-execute-app docker clean all: pi-execute-app pi-execute-app: GOARCH=amd64 go build -o pi-counter-app main.go docker: docker build . -t kubeedge/kubeedge-pi-counter:v1.0.0 clean: rm -f pi-counter-app $ make all $ make docker

部署 Pi Counter App

1
$ cd $GOPATH/src/github.com/kubeedge/examples/kubeedge-counter-demo/crds $ kubectl apply -f kubeedge-pi-counter-app.yaml # 说明:为了防止Pod的部署卡在`ContainerCreating`,这里直接通过docker save、scp和docker load命令将镜像发布到边缘端 $ docker save -o kubeedge-pi-counter.tar kubeedge/kubeedge-pi-counter:v1.0.0 $ scp kubeedge-pi-counter.tar root@192.168.159.201:/home # 以下在边缘端执行 $ cd / $ docker load -i kubeedge-pi-counter.tar

5.4 体验 demo

现在,KubeEdge Demo 的云端部分和边缘端的部分都已经部署完毕,如下:

1
$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kube-7fcbcfdf95-84tjw 1/1 Running 0 4d5h 10.2.1.6 node1 <none> <none> kube-7fcbcfdf95-g4dzs 1/1 Running 0 4d5h 10.2.2.6 node2 <none> <none> kubeedge-counter-app-758b9b4ffd-28xq4 1/1 Running 0 4m34s 192.168.159.101 master1 <none> <none> kubeedge-pi-counter-c69698d6-l7dmt 1/1 Running 0 107s 192.168.159.201 edge1 <none> <none>

访问测试

因为使用的 hostNetwork 模式,所以直接访问即可 。
在这里插入图片描述

选择ON点击Execute

在边缘节点查看执行情况:

1
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8ced25dc67e4 0c040c9b34a0 "/pi-counter-app pi-…" 5 minutes ago Up 5 minutes k8s_kubeedge-pi-counter_kubeedge-pi-counter-c69698d6-l7dmt_default_c01ecd79-daa6-4b6b-a600-f6c71f228167_0 88f4400bda21 kubeedge/pause:3.1 "/pause" 6 minutes ago Up 6 minutes k8s_POD_kubeedge-pi-counter-c69698d6-l7dmt_default_c01ecd79-daa6-4b6b-a600-f6c71f228167_0 $ docker logs -f 8ced25dc67e4

image-20201104231652269

5.5 源码分析

5.5.1 代码结构

代码主要包含3部分:

1、 web-contoller-app:云端应用,用以控制边缘端

2 、counter-mapper:边缘端应用,用以计数

3 、crds:资源文件,用以部署应用

(1)kubeedge-web-controller-app.yaml:用以部署云端web应用

(2)kubeedge-pi-counter-app.yaml:用以部署边缘端应用

(3)kubeedge-counter-model.yaml:Device Model,描述设备的元信息,包括序列号、资产标识符、Mac地址等描述设备的详细信息,也可以称为设备的静态属性或设备属性。

(4)kubeedge-counter-instance.yaml:Device,描述设备的动态数据,包括特定背景下的设备专有实时数据,例如灯的开、关状态,也可以称为设备的孪生属性。设备孪生同时记录设备的Actual State(真实状态)和Expected State(期望状态),这种方式也使设备在离线状况下再次上线时,设备的状态也能得到同步。

1
status: twins: - propertyName: status desired: # 期望状态 metadata: type: string value: 'OFF' reported: # 真实状态 metadata: type: string value: '0'

5.5.2 代码说明

5.5.2.1 云端应用:web-controller-app

1、部署文件:kubeedge-web-controller-app.yaml

2、web开发采用Beego框架

主要页面为views\content.html,主要JS为static\js\track.js,页面控制类为controller\trackController.go。

3、应用提供3个功能:开、关边缘端的计数功能,以及获取当前计数。

4、通过调用k8s API,逐层往下调用(远端、边缘端、MQTT), 最终实现与边缘端交互的目的。

5.5.2.2 边缘端应用:counter-mapper

1、主程序入口为main.go的main方法

2、通过订阅消息$hw/events/device/counter/twin/update获取来自云端的指令

50行代码实现的一个最简单的基于 DirectShow 的视频播放器-CSDN博客
最简单的基于DirectShow的示例:视频播放器_directshow 雷 霄-CSDN博客
最简单的基于DirectShow的示例:视频播放器图形界面版-CSDN博客
最简单的基于DirectShow的示例:视频播放器自定义版_directshow demuxer-CSDN博客
最简单的基于DirectShow的示例:获取Filter信息_pininfo-CSDN博客

对比说明

/文件系统

TFS

FastDFS

MogileFS

MooseFS

GlusterFS

Ceph

开发语言

C++

C

Perl

C

C

C++

开源协议

GPL V2

GPL V3

GPL

GPL V3

GPL V3

LGPL

数据存储方式

文件/Trunk

文件

文件/块

对象/文件/块

集群节点通信协议

私有协议(TCP)

私有协议(TCP)

HTTP

私有协议(TCP)

私有协议(TCP)/ RDAM(远程直接访问内存)

私有协议(TCP)

专用元数据存储点

占用NS

占用DB

占用MFS

占用MDS

在线扩容

支持

支持

支持

支持

支持

支持

冗余备份

支持

支持

-

支持

支持

支持

单点故障

存在

不存在

存在

存在

不存在

存在

跨集群同步

支持

部分支持

-

-

支持

不适用

易用性

安装复杂,官方文档少

安装简单,社区相对活跃

-

安装简单,官方文档多

安装简单,官方文档专业化

安装简单,官方文档专业化

适用场景

跨集群的小文件

单集群的中小文件

-

单集群的大中文件

跨集群云存储

单集群的大中小文件

开源协议说明

GPL:不允许修改后和衍生的代码做为闭源的商业软件发布和销售,修改后该软件产品必须也采用GPL协议;

GPL V2:修改文本的整体就必须按照GPL流通,不仅该修改文本的源码必须向社 会公开,而且对于这种修改文本的流通不准许附加修改者自己作出的限制;

GPL V3:要求用户公布修改的源代码,还要求公布相关硬件;LGPL:更宽松的GPL

TFS(Taobao File System)是由淘宝开发的一个分布式文件系统,其内部经过特殊的优化处理,适用于海量的小文件存储,目前已经对外开源;

TFS采用自有的文件系统格式存储,因此需要专用的API接口去访问,目前官方提供的客户端版本有:C++/JAVA/PHP。

  • 特性

1)在TFS文件系统中,NameServer负责管理文件元数据,通过HA机制实现主备热切换,由于所有元数据都是在内存中,其处理效率非常高效,系统架构也非常简单,管理也很方便;

2)TFS的DataServer作为分部署数据存储节点,同时也具备负载均衡和冗余备份的功能,由于采用自有的文件系统,对小文件会采取合并策略,减少数据碎片,从而提升IO性能;

3)TFS将元数据信息(BlockID、FileID)直接映射至文件名中,这一设计大大降低了存储元数据的内存空间;

  • 优点

1)针对小文件量身定做,随机IO性能比较高;

2)支持在线扩容机制,增强系统的可扩展性;

3)实现了软RAID,增强系统的并发处理能力及数据容错恢复能力;

4)支持主备热倒换,提升系统的可用性;

5)支持主从集群部署,其中从集群主要提供读/备功能;

  • 缺点

1)TFS只对小文件做优化,不适合大文件的存储;

2)不支持POSIX通用接口访问,通用性较低;

3)不支持自定义目录结构,及文件权限控制;

4)通过API下载,存在单点的性能瓶颈;

5)官方文档非常少,学习成本高;

  • 应用场景

1)多集群部署的应用

2)存储后基本不做改动

3)海量小型文件

根据目前官方提供的材料,对单个集群节点,存储节点在1000台以内可以良好工作,如存储节点扩大可能会出现NameServer的性能瓶颈,目前淘宝线上部署容量已达到1800TB规模(2009年数据)

 参考

FastDFS

FastDFS是国人开发的一款分布式文件系统,目前社区比较活跃。如上图所示系统中存在三种节点:Client、Tracker、Storage,在底层存储上通过逻辑的分组概念,使得通过在同组内配置多个Storage,从而实现软RAID10,提升并发IO的性能、简单负载均衡及数据的冗余备份;同时通过线性的添加新的逻辑存储组,从容实现存储容量的线性扩容。

文件下载上,除了支持通过API方式,目前还提供了apache和nginx的插件支持,同时也可以不使用对应的插件,直接以Web静态资源方式对外提供下载。

目前FastDFS(V4.x)代码量大概6w多行,内部的网络模型使用比较成熟的libevent三方库,具备高并发的处理能力。

  • 特性

1)在上述介绍中Tracker服务器是整个系统的核心枢纽,其完成了访问调度(负载均衡),监控管理Storage服务器,由此可见Tracker的作用至关重要,也就增加了系统的单点故障,为此FastDFS支持多个备用的Tracker,虽然实际测试发现备用Tracker运行不是非常完美,但还是能保证系统可用。

2)在文件同步上,只有同组的Storage才做同步,由文件所在的源Storage服务器push至其它Storage服务器,目前同步是采用Binlog方式实现,由于目前底层对同步后的文件不做正确性校验,因此这种同步方式仅适用单个集群点的局部内部网络,如果在公网上使用,肯定会出现损坏文件的情况,需要自行添加文件校验机制。

3)支持主从文件,非常适合存在关联关系的图片,在存储方式上,FastDFS在主从文件ID上做取巧,完成了关联关系的存储。

  • 优点

1)系统无需支持POSIX(可移植操作系统),降低了系统的复杂度,处理效率更高

2)支持在线扩容机制,增强系统的可扩展性

3)实现了软RAID,增强系统的并发处理能力及数据容错恢复能力

4)支持主从文件,支持自定义扩展名

5)主备Tracker服务,增强系统的可用性

  • 缺点

1)不支持断点续传,对大文件将是噩梦(FastDFS不适合大文件存储)

2)不支持POSIX通用接口访问,通用性较低

3)对跨公网的文件同步,存在较大延迟,需要应用做相应的容错策略

4)同步机制不支持文件正确性校验,降低了系统的可用性

5)通过API下载,存在单点的性能瓶颈

  • 应用场景

1)单集群部署的应用

2)存储后基本不做改动

3)小中型文件根据

目前官方提供的材料,现有的使用FastDFS系统存储容量已经达到900T,物理机器已经达到100台(50个组)

  • 参考

MooseFS

MooseFS是一个高可用的故障容错分布式文件系统,它支持通过FUSE方式将文件挂载操作,同时其提供的web管理界面非常方便查看当前的文件存储状态。

  • 特性

1)从下图中我们可以看到MooseFS文件系统由四部分组成:Managing Server 、Data Server 、Metadata Backup Server 及Client

2)其中所有的元数据都是由Managing Server管理,为了提高整个系统的可用性,Metadata Backup Server记录文件元数据操作日志,用于数据的及时恢复

3)Data Server可以分布式部署,存储的数据是以块的方式分布至各存储节点的,因此提升了系统的整体性能,同时Data Server提供了冗余备份的能力,提升系统的可靠性

4)Client通过FUSE方式挂载,提供了类似POSIX的访问方式,从而降低了Client端的开发难度,增强系统的通用性

  • 元数据服务器(master):负责各个数据存储服务器的管理,文件读写调度,文件空间回收以及恢复
  • 元数据日志服务器(metalogger):负责备份master服务器的变化日志文件,以便于在master server出问题的时候接替其进行工作
  • 数据存储服务器(chunkserver):数据实际存储的地方,由多个物理服务器组成,负责连接管理服务器,听从管理服务器调度,提供存储空间,并为客户提供数据传输;多节点拷贝;在数据存储目录,看不见实际的数据

  • 优点

1)部署安装非常简单,管理方便

2)支持在线扩容机制,增强系统的可扩展性

3)实现了软RAID,增强系统的 并发处理能力及数据容错恢复能力

4)数据恢复比较容易,增强系统的可用性5)有回收站功能,方便业务定制

  • 缺点

1)存在单点性能瓶颈及单点故障

2)MFS Master节点很消耗内存

3)对于小于64KB的文件,存储利用率较低

  • 应用场景

  • 参考

GlusterFS

GlusterFS是Red Hat旗下的一款开源分布式文件系统,它具备高扩展、高可用及高性能等特性,由于其无元数据服务器的设计,使其真正实现了线性的扩展能力,使存储总容量可 轻松达到PB级别,支持数千客户端并发访问;对跨集群,其强大的Geo-Replication可以实现集群间数据镜像,而且是支持链式复制,这非常适用 于垮集群的应用场景

  • 特性

1)目前GlusterFS支持FUSE方式挂载,可以通过标准的NFS/SMB/CIFS协议像访问本体文件一样访问文件系统,同时其也支持HTTP/FTP/GlusterFS访问,同时最新版本支持接入Amazon的AWS系统

2)GlusterFS系统通过基于SSH的命令行管理界面,可以远程添加、删除存储节点,也可以监控当前存储节点的使用状态

3)GlusterFS支持集群节点中存储虚拟卷的扩容动态扩容;同时在分布式冗余模式下,具备自愈管理功能,在Geo冗余模式下,文件支持断点续传、异步传输及增量传送等特点

  • 优点

1)系统支持POSIX(可移植操作系统),支持FUSE挂载通过多种协议访问,通用性比较高

2)支持在线扩容机制,增强系统的可扩展性

3)实现了软RAID,增强系统的 并发处理能力及数据容错恢复能力

4)强大的命令行管理,降低学习、部署成本

5)支持整个集群镜像拷贝,方便根据业务压力,增加集群节点

6)官方资料文档专业化,该文件系统由Red Hat企业级做维护,版本质量有保障

  • 缺点

1)通用性越强,其跨越的层次就越多,影响其IO处理效率

2)频繁读写下,会产生垃圾文件,占用磁盘空间

  • 应用场景

1)多集群部署的应用

2)中大型文件根据目前官方提供的材料,现有的使用GlusterFS系统存储容量可轻松达到PB

  • 术语:

brick:分配到卷上的文件系统块;

client:挂载卷,并对外提供服务;

server:实际文件存储的地方;

subvolume:被转换过的文件系统块;

volume:最终转换后的文件系统卷。

  • 参考

Ceph是一个可以按对象/块/文件方式存储的开源分布式文件系统,其设计之初,就将单点故障作为首先要解决的问题,因此该系统具备高可用性、高性能及可 扩展等特点。该文件系统支持目前还处于试验阶段的高性能文件系统BTRFS(B-Tree文件系统),同时支持按OSD方式存储,因此其性能是很卓越的, 因为该系统处于试商用阶段,需谨慎引入到生产环境

  • 特性

1)Ceph底层存储是基于RADOS(可靠的、自动的分布式对象存储),它提供了LIBRADOS/RADOSGW/RBD/CEPH FS方式访问底层的存储系统,如下图所示

2)通过FUSE,Ceph支持类似的POSIX访问方式;Ceph分布式系统中最关键的MDS节点是可以部署多台,无单点故障的问题,且处理性能大大提升

3)Ceph通过使用CRUSH算法动态完成文件inode number到object number的转换,从而避免再存储文件metadata信息,增强系统的灵活性

  • 优点

1)支持对象存储(OSD)集群,通过CRUSH算法,完成文件动态定位, 处理效率更高

2)支持通过FUSE方式挂载,降低客户端的开发成本,通用性高

3)支持分布式的MDS/MON,无单点故障

4)强大的容错处理和自愈能力5)支持在线扩容和冗余备份,增强系统的可靠性

  • 缺点

1)目前处于试验阶段,系统稳定性有待考究

  • 应用场景

1)全网分布式部署的应用

2)对实时性、可靠性要求比较高官方宣传,存储容量可轻松达到PB级别

  • 参考

  • 开发语言:perl

  • 开源协议:GPL

  • 依赖数据库

  • Trackers(控制中心):负责读写数据库,作为代理复制storage间同步的数据

  • Database:存储源数据(默认mysql)

  • Storage:文件存储

  • 除了API,可以通过与nginx集成,对外提供下载服务

  • 参考

 其它参考

http://geek.csdn.net/news/detail/108566
【作者】张辉,就职于携程技术中心信息安全部,负责安全产品的设计与研发。

作为互联网公司的信息安全从业人员经常要处理撞库扫号事件,产生撞库扫号的根本原因是一些企业发生了信息泄露事件,且这些泄露数据未加密或者加密方式比较弱,导致黑客可以还原出原始的用户密码。目前已经曝光的信息泄露事件至少上百起,其中包括多家一线互联网公司,泄露总数据超过10亿条。

要完全防止信息泄露是非常困难的事情,除了防止黑客外,还要防止内部人员泄密。但如果采用合适的算法去加密用户密码,即使信息泄露出去,黑客也无法还原出原始的密码(或者还原的代价非常大)。也就是说我们可以将工作重点从防止泄露转换到防止黑客还原出数据。下面我们将分别介绍用户密码的加密方式以及主要的破解方法。

一、用户密码加密
用户密码保存到数据库时,常见的加密方式有哪些,我们该采用什么方式来保护用户的密码呢?以下几种方式是常见的密码保存方式:

1、直接明文保存,比如用户设置的密码是“123456”,直接将“123456”保存在数据库中,这种是最简单的保存方式,也是最不安全的方式。但实际上不少互联网公司,都可能采取的是这种方式。
2、使用对称加密算法来保存,比如3DES、AES等算法,使用这种方式加密是可以通过解密来还原出原始密码的,当然前提条件是需要获取到密钥。不过既然大量的用户信息已经泄露了,密钥很可能也会泄露,当然可以将一般数据和密钥分开存储、分开管理,但要完全保护好密钥也是一件非常复杂的事情,所以这种方式并不是很好的方式。

这里写图片描述

3、使用MD5、SHA1等单向HASH算法保护密码,使用这些算法后,无法通过计算还原出原始密码,而且实现比较简单,因此很多互联网公司都采用这种方式保存用户密码,曾经这种方式也是比较安全的方式,但随着彩虹表技术的兴起,可以建立彩虹表进行查表破解,目前这种方式已经很不安全了。

这里写图片描述

4、特殊的单向HASH算法,由于单向HASH算法在保护密码方面不再安全,于是有些公司在单向HASH算法基础上进行了加盐、多次HASH等扩展,这些方式可以在一定程度上增加破解难度,对于加了“固定盐”的HASH算法,需要保护“盐”不能泄露,这就会遇到“保护对称密钥”一样的问题,一旦“盐”泄露,根据“盐”重新建立彩虹表可以进行破解,对于多次HASH,也只是增加了破解的时间,并没有本质上的提升。

图片描述
5、PBKDF2算法,该算法原理大致相当于在HASH算法基础上增加随机盐,并进行多次HASH运算,随机盐使得彩虹表的建表难度大幅增加,而多次HASH也使得建表和破解的难度都大幅增加。使用PBKDF2算法时,HASH算法一般选用sha1或者sha256,随机盐的长度一般不能少于8字节,HASH次数至少也要1000次,这样安全性才足够高。一次密码验证过程进行1000次HASH运算,对服务器来说可能只需要1ms,但对于破解者来说计算成本增加了1000倍,而至少8字节随机盐,更是把建表难度提升了N个数量级,使得大批量的破解密码几乎不可行,该算法也是美国国家标准与技术研究院推荐使用的算法。

图片描述
6、bcrypt、scrypt等算法,这两种算法也可以有效抵御彩虹表,使用这两种算法时也需要指定相应的参数,使破解难度增加。

下表对比了各个算法的特性:

这里写图片描述

二、用户密码破解
用户密码破解需要针对具体的加密方式来实施,如果使用对称加密,并且算法足够安全(比如AES),必须获取到密钥才能解密,没有其它可行的破解方式。

如果采用HASH算法(包括特殊HASH),一般使用彩虹表的方式来破解,彩虹表的原理是什么呢?我们先来了解下如何进行HASH碰撞。单向HASH算法由于不能进行解密运算,只能通过建表、查表的方式进行碰撞,即将常用的密码及其对应的HASH值全计算出来并存储,当获取到HASH值是,直接查表获取原始密码,假设用MD5算法来保护6位数字密码,可以建如下表:

这里写图片描述

全表共100W条记录,因为数据量不大,这种情况建表、查表都非常容易。但是当密码并不是6位纯数字密码,而是数字、大小写字母结合的10位密码时,建立一个这样的表需要(26+26+10)^ 10 ≈ 83亿亿(条记录),存储在硬盘上至少要占用2000W TB的空间,这么大的存储空间,成本太大,几乎不可行。有什么办法可以减少存储空间?一种方法是“预计算哈希链”,“预计算哈希链”可以大幅减少HASH表的存储空间,但相应的增加了查表时的计算量,其原理大致如下:

建表过程:

这里写图片描述
先对原始数据“000000”进行一次HASH运算得到“670B1E”,再对HASH值进行一次R运算,R是一个定制的算法可以将HASH值映射到明文空间上(这里我们的明文空间是000000~999999),R运算后得到“283651”,再对“283651”进行hash运算得到“1A99CD”,然后在进行R运算得到“819287”,如此重复多次,得到一条哈希链。然后再选用其它原始数据建立多条哈希链。最终仅将链头和链尾保存下来,中间节点全都去掉。

查表过程:假设拿到了一条HASH值“670B1E”,首先进行一次R运算,得到了“283651”,查询所有链尾是否有命中,如果没有,则再进行一次HASH、一次R,得到了“819287”,再次所有链尾,可以得到看出已经命中。这样我们就可以基本确认“670B1E”对应的明文就在这条链上,然后我们把这条链的生成过程进行重新计算,计算过程中可以发现“000000”的HASH值就是“670B1E”,这样就完成了整个查表过程。这种表就是“预计算哈希链”。这种方式存在一个问题,多条链之间可能存在大量的重复数据,如下图所示:

这里写图片描述
为了解决这个问题,我们将R算法进行扩展,一条链上的多次R运算采用不同的算法,如下图:

这里写图片描述

一条链上的每个R算法都不一样,就像彩虹的每层颜色一样,因此取名的为彩虹表。

当然彩虹表除了可以用户破解HASH算法外,理论上还可以用于破解对称加密算法,比如DES算法,由于DES算法密钥比较短,建立彩虹表破解是完全可行的;但对于AES算法,由于密钥比较长,建表几乎不可行(需要耗时N亿年)。

三、小结
采用PBKDF2、bcrypt、scrypt等算法可以有效抵御彩虹表攻击,即使数据泄露,最关键的“用户密码”仍然可以得到有效的保护,黑客无法大批量破解用户密码,从而切断撞库扫号的根源。当然,对于已经泄露的密码,还是需要用户尽快修改密码,不要再使用已泄露的密码。

彻底解决 gcr、quay、DockerHub 镜像下载难题!-腾讯云开发者社区-腾讯云

Excerpt

作者 | 米开朗基杨

来源 | https://mp.weixin.qq.com/s/GLXUgR75YAqFWC-SMEIkJg
前言
在使用 Docker 和 Kubernetes 时,我们经常需要访问 gcr.io 和 quay.io 镜像仓库,由于众所周知的原因,这些镜像仓库在中国都无法访问,唯一能访问的是 …


作者 | 米开朗基杨

来源 | https://mp.weixin.qq.com/s/GLXUgR75YAqFWC-SMEIkJg

前言

在使用 Docker 和 Kubernetes 时,我们经常需要访问 gcr.io 和 quay.io 镜像仓库,由于众所周知的原因,这些镜像仓库在中国都无法访问,唯一能访问的是 Docker Hub,但速度也是奇慢无比。gcr.azk8s.cn 是 gcr.io 镜像仓库的代理站点,原来可以通过 gcr.azk8s.cn 访问 gcr.io 仓库里的镜像,但是目前 *.azk8s.cn 已经仅限于 Azure 中国的 IP 使用,不再对外提供服务了。国内其他的镜像加速方案大多都是采用定时同步的方式来缓存,这种方法是有一定延迟的,不能保证及时更新,ustc 和七牛云等镜像加速器我都试过了,非常不靠谱,很多镜像都没有。

为了能够顺利访问 gcr.io 等镜像仓库,我们需要在墙外自己搭建一个类似于 gcr.azk8s.cn 的镜像仓库代理站点。利用 Docker 的开源项目 registry[1] 就可以实现这个需求,registry 不仅可以作为本地私有镜像仓库,还可以作为上游镜像仓库的缓存,也就是 pull through cache

先来感受下速度:

1. 前提条件

  • 一台能够施展魔法的服务器(你懂得,可以直接访问 gcr.io)
  • 一个域名和域名相关的 SSL 证书(docker pull 镜像时需要验证域名证书),一般用 Let’s Encrypt[2] 就够了。

2. 核心思路

registry 可以通过设置参数 remoteurl 将其作为远端仓库的缓存仓库,这样当你通过这个私有仓库的地址拉取镜像时,regiistry 会先将镜像缓存到本地存储,然后再提供给拉取的客户端(有可能这两个步骤是同时的,我也不太清楚)。我们可以先部署一个私有 registry,然后将 remoteurl 设为需要加速的镜像仓库地址,基本上就可以了。

3. 定制 registry

为了能够支持缓存 docker.iogcr.iok8s.gcr.ioquay.io 和 ghcr.io 等常见的公共镜像仓库,我们需要对 registry 的配置文件进行定制,Dockerfile 如下:

1
FROM registry:2.6 LABEL maintainer="registry-proxy Docker Maintainers https://fuckcloudnative.io" ENV PROXY_REMOTE_URL="" \ DELETE_ENABLED="" COPY entrypoint.sh /entrypoint.sh

其中 entrypoint.sh 用来将环境变量传入配置文件:

entrypoint.sh

1
#!/bin/sh set -e CONFIG_YML=/etc/docker/registry/config.yml if [ -n "$PROXY_REMOTE_URL" -a `grep -c "$PROXY_REMOTE_URL" $CONFIG_YML` -eq 0 ]; then echo "proxy:" >> $CONFIG_YML echo " remoteurl: $PROXY_REMOTE_URL" >> $CONFIG_YML echo " username: $PROXY_USERNAME" >> $CONFIG_YML echo " password: $PROXY_PASSWORD" >> $CONFIG_YML echo "------ Enabled proxy to remote: $PROXY_REMOTE_URL ------" elif [ $DELETE_ENABLED = true -a `grep -c "delete:" $CONFIG_YML` -eq 0 ]; then sed -i '/rootdirectory/a\ delete:' $CONFIG_YML sed -i '/delete/a\ enabled: true' $CONFIG_YML echo "------ Enabled local storage delete -----" fi sed -i "/headers/a\ Access-Control-Allow-Origin: ['*']" $CONFIG_YML sed -i "/headers/a\ Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']" $CONFIG_YML sed -i "/headers/a\ Access-Control-Expose-Headers: ['Docker-Content-Digest']" $CONFIG_YML case "$1" in *.yaml|*.yml) set -- registry serve "$@" ;; serve|garbage-collect|help|-*) set -- registry "$@" ;; esac exec "$@"

4. 启动缓存服务

构建好 Docker 镜像之后,就可以启动服务了。如果你不想自己构建,可以直接用我的镜像:yangchuansheng/registry-proxy

一般来说,即使你要同时缓存 docker.iogcr.iok8s.gcr.ioquay.io 和 ghcr.io,一台 1C 2G 的云主机也足够了(前提是你不在上面跑其他的服务)。我的博客、评论服务和其他一堆乱七八糟的服务都要跑在云主机上,所以一台是不满足我的需求的,我直接买了两台腾讯云香港轻量级服务器。

既然买了两台,肯定得组个 k3s 集群啦,看主机名就知道我是用来干啥的。其中 2C 4G 作为 master 节点,1C 2G 作为 node 节点。

以 docker.io 为例,创建资源清单:

dockerhub.yaml

1
apiVersion: apps/v1 kind: Deployment metadata: name: dockerhub labels: app: dockerhub spec: replicas: 1 selector: matchLabels: app: dockerhub template: metadata: labels: app: dockerhub spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - dockerhub topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: dockerhub image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://registry-1.docker.io - name: PROXY_USERNAME value: yangchuansheng - name: PROXY_PASSWORD value: ******** ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: dockerhub labels: app: dockerhub spec: selector: app: dockerhub ports: - protocol: TCP name: http port: 5000 targetPort: 5000

使用资源清单创建对应的服务:

1
🐳 → kubectl apply -f dockerhub.yaml

如果你只有一台主机,可以使用 docker-compose 来编排容器,配置文件可以自己参考 k8s 的配置修改,本文就不赘述了。

5. 代理选择

如果只缓存 docker.io,可以直接将 registry-proxy 的端口改成 443,并添加 SSL 证书配置。如果要缓存多个公共镜像仓库,就不太推荐这么做了,因为 443 端口只有一个,多个 registry-proxy 服务不能共用一个端口,合理的做法是使用边缘代理服务根据域名来转发请求到不同的 registry-proxy 服务。

对于 Kubernetes 集群来说,Ingress Controller 即边缘代理,常见的 Ingress Controller 基本上都是由 Nginx 或者 Envoy 来实现。Envoy 虽为代理界新秀,但生而逢时,它的很多特性都是原生为云准备的,是真正意义上的 Cloud Native L7 代理和通信总线。比如它的服务发现和动态配置功能,与 Nginx 等代理的热加载不同,Envoy 可以通过 API 来实现其控制平面,控制平面可以集中服务发现,并通过 API 接口动态更新数据平面的配置,不需要重启数据平面的代理。不仅如此,控制平面还可以通过 API 将配置进行分层,然后逐层更新。

目前使用 Envoy 实现的 Ingress Controller 有 Contour 和 Gloo[3] 等,如果你对 Envoy 比较感兴趣,并且想使用 Ingress Controller 作为边缘代理,可以试试 Contour。Ingress Controller 对底层做了抽象,屏蔽了很多细节,无法顾及到所有细节的配置,必然不会支持底层代理所有的配置项,所以我选择使用原生的 Envoy 来作为边缘代理。如果你是单机跑的 registry-proxy 服务,也可以试试 Envoy。

6. 代理配置

首先创建 Envoy 的资源清单:

envoy.yaml

1
apiVersion: apps/v1 kind: Deployment metadata: name: envoy namespace: kube-system labels: app: envoy spec: replicas: 2 selector: matchLabels: app: envoy strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: envoy spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet containers: - name: envoy image: envoyproxy/envoy:v1.17-latest imagePullPolicy: IfNotPresent command: - envoy - /etc/envoy/envoy.yaml ports: - containerPort: 443 name: https - containerPort: 80 name: http - containerPort: 15001 name: http-metrics volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /etc/envoy name: envoy - mountPath: /root/.acme.sh/fuckcloudnative.io name: ssl volumes: - name: localtime hostPath: path: /etc/localtime - name: ssl hostPath: path: /root/.acme.sh/fuckcloudnative.io - name: envoy hostPath: path: /etc/envoy

使用资源清单创建对应的服务:

1
🐳 → kubectl apply -f envoy.yaml

这里选择使用 hostPath 将 envoy 的配置挂载到容器中,然后通过文件来动态更新配置。来看下 Envoy 的配置,先进入 /etc/envoy 目录。

bootstrap 配置:

envoy.yaml

1
node: id: node0 cluster: cluster0 dynamic_resources: lds_config: path: /etc/envoy/lds.yaml cds_config: path: /etc/envoy/cds.yaml admin: access_log_path: "/dev/stdout" address: socket_address: address: "0.0.0.0" port_value: 15001

LDS 的配置:

lds.yaml

1
version_info: "0" resources: - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: listener_http address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO access_log: name: envoy.access_loggers.file typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout route_config: name: http_route virtual_hosts: - name: default domains: - "*" routes: - match: prefix: "/" redirect: https_redirect: true port_redirect: 443 response_code: "FOUND" http_filters: - name: envoy.filters.http.router - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: listener_https address: socket_address: address: 0.0.0.0 port_value: 443 listener_filters: - name: "envoy.filters.listener.tls_inspector" typed_config: {} filter_chains: - transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext common_tls_context: alpn_protocols: h2,http/1.1 tls_certificates: - certificate_chain: filename: "/root/.acme.sh/fuckcloudnative.io/fullchain.cer" private_key: filename: "/root/.acme.sh/fuckcloudnative.io/fuckcloudnative.io.key" filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_https codec_type: AUTO use_remote_address: true access_log: name: envoy.access_loggers.file typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout route_config: name: https_route response_headers_to_add: - header: key: Strict-Transport-Security value: "max-age=15552000; includeSubdomains; preload" virtual_hosts: - name: docker domains: - docker.fuckcloudnative.io routes: - match: prefix: "/" route: cluster: dockerhub timeout: 600s http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

CDS 的配置:

cds.yaml

1
version_info: "0" resources: - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: dockerhub connect_timeout: 15s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: dockerhub endpoints: - lb_endpoints: - endpoint: address: socket_address: address: dockerhub.default port_value: 5000

这里的 address 使用的是 Kubernetes 集群内部域名,其他部署方式请自己斟酌。

配置好了 Envoy 之后,就可以通过代理服务器拉取 docker.io 的镜像了。

7. 验证加速效果

现在你就可以通过代理服务器来拉取公共镜像了。比如你想拉取 nginx:alpine 镜像,可以使用下面的命令:

1
🐳 → docker pull docker.fuckcloudnative.io/library/nginx:alpine alpine: Pulling from library/nginx 801bfaa63ef2: Pull complete b1242e25d284: Pull complete 7453d3e6b909: Pull complete 07ce7418c4f8: Pull complete e295e0624aa3: Pull complete Digest: sha256:c2ce58e024275728b00a554ac25628af25c54782865b3487b11c21cafb7fabda Status: Downloaded newer image for docker.fuckcloudnative.io/library/nginx:alpine docker.fuckcloudnative.io/library/nginx:alpine

8. 缓存所有镜像仓库

前面的示例只是缓存了 docker.io,如果要缓存所有的公共镜像仓库,可以参考 4-6 节的内容。以 k8s.gcr.io 为例,先准备一个资源清单:

gcr-k8s.yaml

1
apiVersion: apps/v1 kind: Deployment metadata: name: gcr-k8s labels: app: gcr-k8s spec: replicas: 1 selector: matchLabels: app: gcr-k8s template: metadata: labels: app: gcr-k8s spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - gcr-k8s topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: gcr-k8s image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://k8s.gcr.io ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: gcr-k8s labels: app: gcr-k8s spec: selector: app: gcr-k8s ports: - protocol: TCP name: http port: 5000 targetPort: 5000

将其部署到 Kubernetes 集群中:

1
🐳 → kubectl apply -f gcr-k8s.yaml

在 lds.yaml 中添加相关配置:

1
virtual_hosts: - name: docker ... ... - name: k8s domains: - k8s.fuckcloudnative.io routes: - match: prefix: "/" route: cluster: gcr-k8s timeout: 600s

在 cds.yaml 中添加相关配置:

1
- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: gcr-k8s connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: gcr-k8s endpoints: - lb_endpoints: - endpoint: address: socket_address: address: gcr-k8s.default port_value: 5000

其他镜像仓库可照搬上述步骤,以下是我自己跑的所有缓存服务容器:

1
🐳 → kubectl get pod -o wide gcr-8647ffb586-67c6g 1/1 Running 0 21h 10.42.1.52 blog-k3s02 ghcr-7765f6788b-hxxvc 1/1 Running 0 21h 10.42.1.55 blog-k3s01 dockerhub-94bbb7497-x4zwg 1/1 Running 0 21h 10.42.1.54 blog-k3s02 gcr-k8s-644db84879-7xssb 1/1 Running 0 21h 10.42.1.53 blog-k3s01 quay-559b65848b-ljclb 1/1 Running 0 21h 10.42.0.154 blog-k3s01

9. 容器运行时配置

配置好所有的缓存服务后,就可以通过代理来拉取公共镜像了,只需按照下面的列表替换镜像地址中的字段就行了:

docker.io/xxx/xxx 或 xxx/xxx

|

docker.fuckcloudnative.io/xxx/xxx

|
|

docker.io/library/xxx 或 xxx

|

docker.fuckcloudnative.io/library/xxx

|
|

gcr.io/xxx/xxx

|

gcr.fuckcloudnative.io/xxx/xxx

|
|

k8s.gcr.io/xxx/xxx

|

k8s.fuckcloudnative.io/xxx/xxx

|
|

quay.io/xxx/xxx

|

quay.fuckcloudnative.io/xxx/xxx

|
|

ghcr.io/xxx/xxx

|

ghcr.fuckcloudnative.io/xxx/xxx

|

当然,最好的方式还是直接配置 registry mirror,Docker 只支持配置 docker.io 的 registry mirror,Containerd 和 Podman 支持配置所有镜像仓库的 registry mirror。

Docker

Docker 可以修改配置文件 /etc/docker/daemon.json,添加下面的内容:

1
{ "registry-mirrors": [ "https://docker.fuckcloudnative.io" ] }

然后重启 Docker 服务,就可以直接拉取 docker.io 的镜像了,不需要显示指定代理服务器的地址,Docker 服务本身会自动通过代理服务器去拉取镜像。比如:

1
🐳 → docker pull nginx:alpine 🐳 → docker pull docker.io/library/nginx:alpine
Containerd

Containerd 就比较简单了,它支持任意 registry 的 mirror,只需要修改配置文件 /etc/containerd/config.toml,添加如下的配置:

1
[plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://docker.fuckcloudnative.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"] endpoint = ["https://k8s.fuckcloudnative.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"] endpoint = ["https://gcr.fuckcloudnative.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"] endpoint = ["https://ghcr.fuckcloudnative.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"] endpoint = ["https://quay.fuckcloudnative.io"]

重启 Containerd 服务后,就可以直接拉取所有镜像了,不需要修改任何前缀,Containerd 会根据配置自动选择相应的代理 URL 拉取镜像。

Podman

Podman 也支持任意 registry 的 mirror,只需要修改配置文件 /etc/containers/registries.conf,添加如下的配置:

1
unqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io'] [[registry]] prefix = "docker.io" insecure = true location = "registry-1.docker.io" [[registry.mirror]] location = "docker.fuckcloudnative.io" [[registry]] prefix = "k8s.gcr.io" insecure = true location = "k8s.gcr.io" [[registry.mirror]] location = "k8s.fuckcloudnative.io" [[registry]] prefix = "gcr.io" insecure = true location = "gcr.io" [[registry.mirror]] location = "gcr.fuckcloudnative.io" [[registry]] prefix = "ghcr.io" insecure = true location = "ghcr.io" [[registry.mirror]] location = "ghcr.fuckcloudnative.io" [[registry]] prefix = "quay.io" insecure = true location = "quay.io" [[registry.mirror]] location = "quay.fuckcloudnative.io"

然后就可以直接拉取所有镜像了,不需要修改任何前缀,Podman 会根据配置自动选择相应的代理 URL 拉取镜像。而且 Podman 还有 fallback 机制,上面的配置表示先尝试通过 registry.mirror 中 location 字段的 URL 来拉取镜像,如果失败就会尝试通过 registry 中 location 字段的 URL 来拉取。

10. 清理缓存

缓存服务会将拉取的镜像缓存到本地,所以需要消耗磁盘容量。一般云主机的磁盘容量都不是很大,OSS 和 s3 存储都比较贵,不太划算。

为了解决这个问题,我推荐定期删除缓存到本地磁盘的部分镜像,或者删除所有镜像。方法也比较简单,单独再部署一个 registry,共用其他 registry 的存储,并启用 delete 功能,然后再通过 API 或者 Dashboard 进行删除。

先准备一个资源清单:

reg-local.yaml

1
apiVersion: apps/v1 kind: Deployment metadata: name: reg-local labels: app: reg-local spec: replicas: 1 selector: matchLabels: app: reg-local template: metadata: labels: app: reg-local spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - reg-local topologyKey: kubernetes.io/hostname weight: 1 containers: - name: reg-local image: yangchuansheng/registry-proxy:latest env: - name: DELETE_ENABLED value: "true" ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: reg-local labels: app: reg-local spec: selector: app: reg-local ports: - protocol: TCP name: http port: 5000 targetPort: 5000

将其部署到 Kubernetes 集群中:

1
🐳 → kubectl apply -f reg-local.yaml

再准备一个 Docker Registry UI 的资源清单:

registry-ui

1
apiVersion: apps/v1 kind: Deployment metadata: name: registry-ui labels: app: registry-ui spec: replicas: 1 selector: matchLabels: app: registry-ui template: metadata: labels: app: registry-ui spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - registry-ui topologyKey: kubernetes.io/hostname weight: 1 tolerations: - key: node-role.kubernetes.io/ingress operator: Exists effect: NoSchedule containers: - name: registry-ui image: joxit/docker-registry-ui:static env: - name: REGISTRY_TITLE value: My Private Docker Registry - name: REGISTRY_URL value: "http://reg-local:5000" - name: DELETE_IMAGES value: "true" ports: - containerPort: 80 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime volumes: - name: localtime hostPath: path: /etc/localtime --- apiVersion: v1 kind: Service metadata: name: registry-ui labels: app: registry-ui spec: selector: app: registry-ui ports: - protocol: TCP name: http port: 80 targetPort: 80

将其部署到 Kubernetes 集群中:

1
🐳 → kubectl apply -f registry-ui.yaml

这样就可以通过 Dashboard 来清理镜像释放空间了。

或者直接简单粗暴,定时删除整个存储目录的内容。例如,执行命令 crontab -e,添加如下内容:

1
* * */2 * * /usr/bin/rm -rf /var/lib/registry/* &>/dev/null

表示每过两天清理一次 /var/lib/registry/ 目录。

11. 防白嫖认证

最后还有一个问题,我把缓存服务的域名全部公开了,如果大家都来白嫖,我的云主机肯定承受不住。为了防止白嫖,我得给 registry-proxy 加个认证,最简单的方法就是使用 basic auth,用 htpasswd 来存储密码。

  1. 为用户 admin 创建一个密码文件,密码为 admin
1
🐳 → docker run \ --entrypoint htpasswd \ registry:2.6 -Bbn admin admin > htpasswd
  1. 创建 Secret:
1
🐳 → kubectl create secret generic registry-auth --from-file=htpasswd
  1. 修改资源清单的配置,以 docker.io 为例:
1
apiVersion: apps/v1 kind: Deployment metadata: name: dockerhub labels: app: dockerhub spec: replicas: 1 selector: matchLabels: app: dockerhub template: metadata: labels: app: dockerhub spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - dockerhub topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: dockerhub image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://registry-1.docker.io - name: PROXY_USERNAME value: yangchuansheng - name: PROXY_PASSWORD value: ******** + - name: REGISTRY_AUTH_HTPASSWD_REALM + value: Registry Realm + - name: REGISTRY_AUTH_HTPASSWD_PATH + value: /auth/htpasswd ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry + - mountPath: /auth + name: auth volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry + - name: auth + secret: + secretName: registry-auth

apply 使其生效:

1
🐳 → kubectl apply -f dockerhub.yaml
  1. 尝试拉取镜像:
1
🐳 → docker pull docker.fuckcloudnative.io/library/nginx:latest Error response from daemon: Get https://docker.fuckcloudnative.io/v2/library/nginx/manifests/latest: no basic auth credentials
  1. 登录镜像仓库:
1
🐳 → docker login docker.fuckcloudnative.io Username: admin Password: WARNING! Your password will be stored unencrypted in /root/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded

现在就可以正常拉取镜像了。

如果你想更细粒度地控制权限,可以使用 Token 的方式来进行认证,具体可以参考 docker_auth[4] 这个项目。

12. 费用评估

好了,现在我们来评估一下这一切的费用。首先你得有一个会魔法的服务器,国内的肯定不用考虑了,必须选择国外的,而且到国内的速度还过得去的,最低最低不会低于 30 人民币/月 吧。除此之外,你还得拥有一个个人域名,这个价格不好说,总而言之,加起来肯定不会低于 30 吧,多数人肯定是下不去这个手的。没关系,我有一个更便宜的方案,我已经部署好了一切,你可以直接用我的服务,当然我也是自己买的服务器,每个月也是要花钱的,如果你真的想用,只需要每月支付 3 元,以此来保障我每个月的服务器费用。当然肯定不止你一个人,目前大概有十几个用户,后面如果人数特别多,再考虑加服务器。这个需要你自己考虑清楚,有意者关注文末公众号向我咨询。

参考资料

[1] registry: https://docs.docker.com/registry/

[2] Let’s Encrypt: https://letsencrypt.org/

[3] Gloo: https://github.com/solo-io/gloo

[4] docker_auth: https://github.com/cesanta/docker\_auth

听过云原生玩家都关注了他

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具

只需一条命令,连 ssh 都不需要!

往期推荐

合格的后端Coder都应该写好UT和Mock测试

Spring Boot 2.4.3、2.3.9 版本发布,你准备好了吗?

打造全球最大规模 Kafka 集群,Uber 的多区域灾备实践

“智能”坐垫记录离座时间,是高科技福利还是又一个员工压榨机器?

Java延迟加载的最佳实践应用示例!

不容错过的灰度发布系统架构设计

本文参与 腾讯云自媒体分享计划,分享自微信公众号。

原始发表:2021-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

插件式程序开发sharpdevelop(1)_c# mef sharpdevelop-CSDN博客

Excerpt

文章浏览阅读2.2k次。在c#,WPF的程序开发中,设计了很多的程序架构,工程类库,设计了多种接口和模式。最终还是觉得微内核+插件式的形式更适用于国内的软件开发,包括各种应用系统。先 用内核插件的方式,搭建最小系统,然后根据 “多变” 的项目需求,设计插件模块,进行动态引用。在最终发布时,根据情况,将一些可以固定的模块进行静态引用划分。 插件内核的开发也需要划分模块,这是关键点。 目前_c# mef sharpdevelop


特性变幻 于 2017-03-08 09:37:10 发布

         在c#,WPF的程序开发中,设计了很多的程序架构,工程类库,设计了多种接口和模式。最终还是觉得微内核+插件式的形式更适用于国内的软件开发,包括各种应用系统。先 用内核插件的方式,搭建最小系统,然后根据 “多变” 的项目需求,设计插件模块,进行动态引用。在最终发布时,根据情况,将一些可以固定的模块进行静态引用划分。

        插件内核的开发也需要划分模块,这是关键点。

        目前针对C#有很多成熟的插件式样构架,如

1、MEF  (Managed Extensibility Framework)
2、MAF  ( Managed Add-in Framework )
3、OSGi.NET (Open Service Gateway Initiative)
4、CSLA.NET Component-based Scalable Logical Architecture
5、SharpDevelop
6、DevExpress XAF
7、Assemble direct

这些框架使用的最底层原理都是反射来实现程序集的动态加载。

最终觉得SharpDevelop更好一些,理由:

1、源码完全开放。

2、完整的插件引用协议描述。

3、完整的架构体系

4、有成熟的多语言、反射集、流程处理、菜单、界面框架等模块。

先期经过调研,已成熟的进入使用,采用sd4.0和5.0进行开发。

接下来针对sd做一系列的技术课题讲解。