用費曼技巧學編程,香不香?

引子

 有一本講諾貝爾獎獲得者,物理學家費曼的書,叫做《發現的樂趣》,書中寫到一個費曼小時候的故事:

 “我們家有《大不列顛百科全書》,我還是小孩子的時候,父親就常常讓我坐在他腿上,給我讀些《大不列顛百科全書》。比如說,我們讀關於恐龍的部分,書上可能講雷龍或其他什麼龍,書上會說:“這傢伙有 25 英尺高,腦袋寬 6 英尺。” 

這時父親就停下來,說:“我們來看看這句話什麼意思。這句話的意思是:假如它站在我們家的前院里,它是那麼高,高到足以把頭從窗戶伸進來。不過呢,它也可能遇到點麻煩,因為它的腦袋比窗戶稍微寬了些,要是它伸進頭來,會擠破窗戶。 

費曼說:凡是我們讀到的東西,我們都盡量把它轉化成某種現實,從這裏我學到一個本領——凡我所讀的內容,我總設法通過某種轉換,弄明白它究竟什麼意思,它到底在說什麼。

 

費曼技巧

 

費曼技巧,或者說費曼學習法是一種以教促學的方法,一共有四步(已經知道的可以無視,直接跳過): 

(1) 選擇新概念/新知識, 自己先去學習它。 

(2) 假裝當一個老師,去教授別人 

想象你面對一群小白,怎麼把這個概念講給他們聽,讓他們理解呢? 

把你講解的思路也寫到紙上,如果實在不想寫,可以說出來。 

非常重要!!!不要讓你的思路停留在大腦中,因為大腦中對於知識點之間的關聯會有些想當然的、錯誤的假設,說出來或者寫出來能找到這些“盲點”!!

 

(3) 如果你在教授的過程中遇到了麻煩,卡了殼,返回去學習。 

重新去看書,搜相關資料,問別人,倒逼自己把這個概念搞清楚, 然後回到第二步,繼續給小白講授。

 

(4) 簡化你的語言。 

目標是用你自己的語言,非專業的詞彙去解釋這個概念。盡量做到簡單直白,或者找到比喻來表達。 

非常簡單的過程,對吧? 

 

實戰演練

我們來用個例子來演練一下,有請碼農翻身頭號主人公張大胖出場。 

張大胖正在學習Java,這一天他遇到了一個新的概念:“動態代理”  (注意是學習這個概念,不是具體實現), 非常抽象,在日常編程中幾乎不會直接使用,理解起來有難度。

 

第一步,自學

 張大胖看了動態代理的介紹,書上列舉出一堆煩人的代碼來展示這個東西是怎麼使用的,比如有個接口(IHelloWorld)及其實現類(HelloWorld), 然後有個InvocationHandler的實現,最後用Proxy.newProxyInstance(….)創建一個新的類出來,這些都是什麼鬼?啰里啰唆的。

 

第二步,張大胖嘗試教一下小白(當然這裏的小白至少得懂點兒Java)

 

張大胖:動態代理嘛,很簡單,就是給定一個接口和實現類,再加上一個InvocationHandler , 動態代理這個技術可以在運行時創建一個新的代理類出來。 

小白:張老師, 新的代理類有什麼用? 

張大胖:舉個例子,有個叫IHelloWorld接口及其實現類HelloWorld,它有一個叫sayHello()的方法。可以在sayHello()之前和之後,額外加一些日誌的輸出。 

(在講解一個概念的時候,舉例和類比很重要,人類習慣於通過例子來學習,從具體走向抽象) 

小白:那我直接寫一個新的類,比如HelloWorldEx,把日誌輸出添加到其中不就行了,為什麼還要用Proxy.newProxyInstance(……)這麼麻煩的方法?

public class HelloWorldEx implements IHelloWorld{
    IHelloWorld hw;
    public HelloWorldEx(IHelloWorld hw){
        this.hw = hw;
    }    
    public void sayHello(){        
        Logger.startLog();
        hw.sayHello();
        Logger.endLog();
    }
}

  

張大胖無法回答這個問題,卡殼了! 

第三步,回過頭去看書,學習。

書中也沒有解釋,唉! 

仔細想一想,手動寫一個類HelloWorldEx和用Proxy.newProxyInstance來創建,區別到底是什麼? 

實現的功能是相同的,但是HelloWorldEx需要事先寫好,編譯后不能改了,相當於寫死了!如果我想對Order類,Employee類,Department類,也想加點兒日誌,還得寫個OrderEx,EmployeeEx,DepartmentEx的類,太麻煩了! 

而Proxy.newProxyInstance這種方法,可以在程序運行的時候為任意類動態地創建增強的類。 

事先寫死的叫做靜態代理,Proxy.newProxyInstance這種方式叫做動態代理,更加靈活。 

張大胖覺得這麼解釋就通了。 

小白:為什麼要創建新的代理類,那個Proxy.newProxyInstance不能直接修改老的HelloWorld類嗎? 

張大胖再度卡殼,上網搜索,找到了答案,和Python,Ruby等方法不同,Java本質是一個靜態類型的語言,class一旦被裝入JVM,是不能修改,添加,刪除方法的,既然老的class不能修改,只能通過代理的方式來創建新的類了。 

小白:懂了,這個技術主要用在什麼地方啊? 難道只是加個日誌? 

張大胖第三次卡殼,只好再次搜索。 

原來動態代理使用得最多的是AOP,AOP中經常會以聲明的方式提出這樣的要求: 

某個包下所有add開頭的方法,在執行之前都要調用Logger.startLog()方法,在執行之後都要調用Logger.endLog()方法。 

或者對於所有以Service結尾的類,所有的方法執行之前都要調用tx.begin(),執行之後都要調用tx.commit(), 如果拋出異常的話調用tx.rollback()。

 

到此為止,張大胖可以這樣來給小白講述了: 

你不是用過Spring AOP嗎?AOP中經常有這樣的需求……  ,Spring想添加這些日誌和事務的功能,但是卻沒有辦法去修改用戶的類,它是框架啊,一是不知道用戶類的源碼,二是Java不允許再修改裝載入JVM的class。 

沒辦法,Spring只好在運行時找到用戶的類,然後操作字節碼動態創建一個新類,新類會對原有的類進行增強,添加日誌,事務這些功能,注意啊,這些都是在內存中動態創建的。 

這個技術就是Java的動態代理,不過它有個前提要求,就是用戶的類需要實現接口才行。我用一個簡單的例子給你說下,你就明白細節了……

 

第四步,簡化,比喻

上面的講解從文字上來說還是非常啰嗦的,用了很大篇幅來講解“為什麼”,因為理解了why ,剩下的就是細節了。  

如果你徹底理解了以後,動態代理的技術細節會在大腦中會建立這麼一幅圖景:

 

$HelloWorld100就是那個代理類,它和HelloWorld都實現了IHelloWorld這個接口。 

如果一定要用個比喻來說,它們倆就是“兄弟關係”,CgLib提供了另外一種對現有類增強的辦法,動態生成的類繼承了現有的類,兩者是“父子關係”。

  

小結

 怎麼樣?用這種(假裝)教授別人,層層遞進、自我逼問的方法是不是很有效果?收益很大?  

用這種辦法,實際上就是逼着你把大腦中的盲點和一些想當然的假設給暴露出來,效果要比單純地閱讀和記憶好得多,趕緊在學習中試一下吧!

  

更多精彩文章,盡在碼農翻身

 

我是一個線程

TCP/IP之大明郵差

一個故事講完Https

CPU 阿甘

Javascript: 一個屌絲的逆襲

微服務把我坑了

如何降低程序員的工資?

程序員,你得選准跑路的時間!

兩年,我學會了所有的編程語言!

一直CRUD,一直996,我煩透了,我要轉型

字節碼萬歲!

上帝託夢給我說:一切皆文件

Node.js :我只需要一個店小二

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

akka-typed(7) – cluster:sharding, 集群分片

  在使用akka-typed的過程中發現有很多地方都簡化了不少,變得更方便了,包括:Supervision,只要用Behaviors.supervise()把Behavior包住,很容易就可以實現這個actor的SupervisorStrategy.restartWithBackoff策略了。然後集群化的group router使用起來也很方便,再就是集群分片cluster-sharding了。下面我們就通過一個例子來介紹cluster-sharding的具體使用方法。

首先,分片的意思是指在集群中多個節點上部署某種actor,即entity,的構建機制。entity的構建是動態的,ClusterSharding系統根據各節點的負載情況決定到底在哪個節點構建entity,然後返回ShardRegion:一個該類entity具體的構建工具及消息中介。也就是說我們可以把同樣的一種運算通過entityId指定給任何一個entity,但具體這個entity生存在集群哪個節點上人工是無法確定的,完全靠ClusterSharding引導。先設計一個簡單功能的actor,測試它作為一個entity的工作細節:

 

object Counter { sealed trait Command extends CborSerializable case object Increment extends Command final case class GetValue(replyTo: ActorRef[Response]) extends Command case object StopCounter extends Command private case object Idle extends Command sealed trait Response extends CborSerializable case class SubTtl(entityId: String, ttl: Int) extends Response val TypeKey = EntityTypeKey[Command]("Counter") def apply(nodeAddress: String, entityContext: EntityContext[Command]): Behavior[Command] = { Behaviors.setup { ctx => def updated(value: Int): Behavior[Command] = { Behaviors.receiveMessage[Command] { case Increment => ctx.log.info("******************{} counting at {},{}",ctx.self.path,nodeAddress,entityContext.entityId) updated(value + 1) case GetValue(replyTo) => ctx.log.info("******************{} get value at {},{}",ctx.self.path,nodeAddress,entityContext.entityId) replyTo ! SubTtl(entityContext.entityId,value) Behaviors.same case Idle => entityContext.shard ! ClusterSharding.Passivate(ctx.self) Behaviors.same case StopCounter => Behaviors.stopped(() => ctx.log.info("************{} stopping ... passivated for idling.", entityContext.entityId)) } } ctx.setReceiveTimeout(30.seconds, Idle) updated(0) } } }

 

cluster-sharding的機制是這樣的:在每個(或指定的)節點上構建部署一個某種EntityType的ShardRegion。這樣系統可以在任何部署了ShardRegion的節點上構建這種entity。然後ClusterSharding系統會根據entityId來引導消息至正確的接收對象。我們再看看ShardRegion的部署是如何實現的吧:

 

object EntityManager { sealed trait Command case class AddOne(counterId: String) extends Command case class GetSum(counterId: String ) extends Command case class WrappedTotal(res: Counter.Response) extends Command def apply(): Behavior[Command] = Behaviors.setup { ctx => val cluster = Cluster(ctx.system) val sharding = ClusterSharding(ctx.system) val entityType = Entity(Counter.TypeKey) { entityContext => Counter(cluster.selfMember.address.toString,entityContext) }.withStopMessage(Counter.StopCounter) sharding.init(entityType) val counterRef: ActorRef[Counter.Response] = ctx.messageAdapter(ref => WrappedTotal(ref)) Behaviors.receiveMessage[Command] { case AddOne(cid) => val entityRef: EntityRef[Counter.Command] = sharding.entityRefFor(Counter.TypeKey, cid) entityRef ! Counter.Increment Behaviors.same case GetSum(cid) => val entityRef: EntityRef[Counter.Command] = sharding.entityRefFor(Counter.TypeKey, cid) entityRef ! Counter.GetValue(counterRef) Behaviors.same case WrappedTotal(ttl) => ttl match { case Counter.SubTtl(eid,subttl) => ctx.log.info("***********************{} total: {} ",eid,subttl) } Behaviors.same } } }

太簡單了, sharding.ini(entityType)一個函數完成了一個節點分片部署。系統通過sharding.init(entityType)來實現ShardRegion構建。這個entityType代表某種特殊actor模版,看看它的構建函數:

object Entity { /** * Defines how the entity should be created. Used in [[ClusterSharding#init]]. More optional * settings can be defined using the `with` methods of the returned [[Entity]]. * * @param typeKey A key that uniquely identifies the type of entity in this cluster * @param createBehavior Create the behavior for an entity given a [[EntityContext]] (includes entityId) * @tparam M The type of message the entity accepts */ def apply[M](typeKey: EntityTypeKey[M])( createBehavior: EntityContext[M] => Behavior[M]): Entity[M, ShardingEnvelope[M]] =
    new Entity(createBehavior, typeKey, None, Props.empty, None, None, None, None, None) }

這個函數需要一個EntityTyeKey和一個構建Behavior的函數createBehavior,產生一個Entity類型。Entity類型定義如下:

final class Entity[M, E] private[akka] ( val createBehavior: EntityContext[M] => Behavior[M], val typeKey: EntityTypeKey[M], val stopMessage: Option[M], val entityProps: Props, val settings: Option[ClusterShardingSettings], val messageExtractor: Option[ShardingMessageExtractor[E, M]], val allocationStrategy: Option[ShardAllocationStrategy], val role: Option[String], val dataCenter: Option[DataCenter]) { /** * [[akka.actor.typed.Props]] of the entity actors, such as dispatcher settings. */ def withEntityProps(newEntityProps: Props): Entity[M, E] = copy(entityProps = newEntityProps) /** * Additional settings, typically loaded from configuration. */ def withSettings(newSettings: ClusterShardingSettings): Entity[M, E] = copy(settings = Option(newSettings)) /** * Message sent to an entity to tell it to stop, e.g. when rebalanced or passivated. * If this is not defined it will be stopped automatically. * It can be useful to define a custom stop message if the entity needs to perform * some asynchronous cleanup or interactions before stopping. */ def withStopMessage(newStopMessage: M): Entity[M, E] = copy(stopMessage = Option(newStopMessage)) /** * * If a `messageExtractor` is not specified the messages are sent to the entities by wrapping * them in [[ShardingEnvelope]] with the entityId of the recipient actor. That envelope * is used by the [[HashCodeMessageExtractor]] for extracting entityId and shardId. The number of * shards is then defined by `numberOfShards` in `ClusterShardingSettings`, which by default * is configured with `akka.cluster.sharding.number-of-shards`. */ def withMessageExtractor[Envelope](newExtractor: ShardingMessageExtractor[Envelope, M]): Entity[M, Envelope] =
    new Entity( createBehavior, typeKey, stopMessage, entityProps, settings, Option(newExtractor), allocationStrategy, role, dataCenter) /** * Allocation strategy which decides on which nodes to allocate new shards, * [[ClusterSharding#defaultShardAllocationStrategy]] is used if this is not specified. */ def withAllocationStrategy(newAllocationStrategy: ShardAllocationStrategy): Entity[M, E] = copy(allocationStrategy = Option(newAllocationStrategy)) /** * Run the Entity actors on nodes with the given role. */ def withRole(newRole: String): Entity[M, E] = copy(role = Some(newRole)) /** * The data center of the cluster nodes where the cluster sharding is running. * If the dataCenter is not specified then the same data center as current node. If the given * dataCenter does not match the data center of the current node the `ShardRegion` will be started * in proxy mode. */ def withDataCenter(newDataCenter: DataCenter): Entity[M, E] = copy(dataCenter = Some(newDataCenter)) private def copy( createBehavior: EntityContext[M] => Behavior[M] = createBehavior, typeKey: EntityTypeKey[M] = typeKey, stopMessage: Option[M] = stopMessage, entityProps: Props = entityProps, settings: Option[ClusterShardingSettings] = settings, allocationStrategy: Option[ShardAllocationStrategy] = allocationStrategy, role: Option[String] = role, dataCenter: Option[DataCenter] = dataCenter): Entity[M, E] = { new Entity( createBehavior, typeKey, stopMessage, entityProps, settings, messageExtractor, allocationStrategy, role, dataCenter) } }

這裏面有許多方法用來控制Entity的構建和作業。

然後我們把這個EntityManager當作RootBehavior部署到多個節點上去:

object ClusterShardingApp { def main(args: Array[String]): Unit = { if (args.isEmpty) { startup("shard", 25251) startup("shard", 25252) startup("shard", 25253) startup("front", 25254) } else { require(args.size == 2, "Usage: role port") startup(args(0), args(1).toInt) } } def startup(role: String, port: Int): Unit = { // Override the configuration of the port when specified as program argument
    val config = ConfigFactory .parseString(s"""       akka.remote.artery.canonical.port=$port akka.cluster.roles = [$role] """)
      .withFallback(ConfigFactory.load("cluster")) val entityManager = ActorSystem[EntityManager.Command](EntityManager(), "ClusterSystem", config) ... }

一共設定了3個role=shard節點和1個front節點。

在front節點上對entityId分別為9013,9014,9015,9016幾個entity發送消息:

 def startup(role: String, port: Int): Unit = { // Override the configuration of the port when specified as program argument
    val config = ConfigFactory .parseString(s"""       akka.remote.artery.canonical.port=$port akka.cluster.roles = [$role] """)
      .withFallback(ConfigFactory.load("cluster")) val entityManager = ActorSystem[EntityManager.Command](EntityManager(), "ClusterSystem", config) if (role == "front") { entityManager ! EntityManager.AddOne("9013") entityManager ! EntityManager.AddOne("9014") entityManager ! EntityManager.AddOne("9013") entityManager ! EntityManager.AddOne("9015") entityManager ! EntityManager.AddOne("9013") entityManager ! EntityManager.AddOne("9014") entityManager ! EntityManager.AddOne("9014") entityManager ! EntityManager.AddOne("9013") entityManager ! EntityManager.AddOne("9015") entityManager ! EntityManager.AddOne("9015") entityManager ! EntityManager.AddOne("9016") entityManager ! EntityManager.GetSum("9014") entityManager ! EntityManager.GetSum("9015") entityManager ! EntityManager.GetSum("9013") entityManager ! EntityManager.GetSum("9016") }

以下是部分運算結果显示:

15:12:10.073 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/786/9014 counting at akka://ClusterSystem@127.0.0.1:25253,9014
15:12:10.106 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/786/9014 counting at akka://ClusterSystem@127.0.0.1:25253,9014
15:12:10.106 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/786/9014 counting at akka://ClusterSystem@127.0.0.1:25253,9014
15:12:10.106 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/785/9013 counting at akka://ClusterSystem@127.0.0.1:25251,9013
15:12:10.107 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/785/9013 counting at akka://ClusterSystem@127.0.0.1:25251,9013
15:12:10.107 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/785/9013 counting at akka://ClusterSystem@127.0.0.1:25251,9013
15:12:10.107 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/785/9013 counting at akka://ClusterSystem@127.0.0.1:25251,9013
15:12:10.109 [ClusterSystem-akka.actor.default-dispatcher-19] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/787/9015 counting at akka://ClusterSystem@127.0.0.1:25254,9015
15:12:10.110 [ClusterSystem-akka.actor.default-dispatcher-19] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/787/9015 counting at akka://ClusterSystem@127.0.0.1:25254,9015
15:12:10.110 [ClusterSystem-akka.actor.default-dispatcher-19] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/787/9015 counting at akka://ClusterSystem@127.0.0.1:25254,9015
15:12:10.110 [ClusterSystem-akka.actor.default-dispatcher-19] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/787/9015 get value at akka://ClusterSystem@127.0.0.1:25254,9015
15:12:10.112 [ClusterSystem-akka.actor.default-dispatcher-18] INFO com.learn.akka.EntityManager$ - ***********************9015 total: 3
15:12:10.149 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/786/9014 get value at akka://ClusterSystem@127.0.0.1:25253,9014
15:12:10.149 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/785/9013 get value at akka://ClusterSystem@127.0.0.1:25251,9013
15:12:10.169 [ClusterSystem-akka.actor.default-dispatcher-18] INFO com.learn.akka.EntityManager$ - ***********************9014 total: 3
15:12:10.169 [ClusterSystem-akka.actor.default-dispatcher-18] INFO com.learn.akka.EntityManager$ - ***********************9013 total: 4
15:12:10.171 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/788/9016 counting at akka://ClusterSystem@127.0.0.1:25251,9016
15:12:10.171 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ******************akka://ClusterSystem/system/sharding/Counter/788/9016 get value at akka://ClusterSystem@127.0.0.1:25251,9016
15:12:10.172 [ClusterSystem-akka.actor.default-dispatcher-18] INFO com.learn.akka.EntityManager$ - ***********************9016 total: 1

15:19:32.176 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ************9013 stopping ... passivated for idling.
15:19:52.529 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ************9014 stopping ... passivated for idling.
15:19:52.658 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.Counter$ - ************9016 stopping ... passivated for idling.
15:19:52.662 [ClusterSystem-akka.actor.default-dispatcher-14] INFO com.learn.akka.Counter$ - ************9015 stopping ... passivated for idling.

下面是本次示範的完整源代碼:

ClusterSharding.scala

package com.learn.akka
import scala.concurrent.duration._
import akka.actor.typed._
import akka.actor.typed.scaladsl._
import akka.cluster.sharding.typed.scaladsl.EntityContext
import akka.cluster.sharding.typed.scaladsl.Entity
import akka.persistence.typed.PersistenceId
//#sharding-extension
import akka.cluster.sharding.typed.ShardingEnvelope
import akka.cluster.sharding.typed.scaladsl.ClusterSharding
import akka.cluster.sharding.typed.scaladsl.EntityTypeKey
import akka.cluster.sharding.typed.scaladsl.EntityRef
import com.typesafe.config.ConfigFactory
import akka.cluster.typed.Cluster
//#counter
object Counter {
  sealed trait Command extends CborSerializable
  case object Increment extends Command
  final case class GetValue(replyTo: ActorRef[Response]) extends Command
  case object StopCounter extends Command
  private case object Idle extends Command

  sealed trait Response extends CborSerializable
  case class SubTtl(entityId: String, ttl: Int) extends Response


  val TypeKey = EntityTypeKey[Command]("Counter")

  def apply(nodeAddress: String, entityContext: EntityContext[Command]): Behavior[Command] = {
    Behaviors.setup { ctx =>
      def updated(value: Int): Behavior[Command] = {
        Behaviors.receiveMessage[Command] {
          case Increment =>
            ctx.log.info("******************{} counting at {},{}",ctx.self.path,nodeAddress,entityContext.entityId)
            updated(value + 1)
          case GetValue(replyTo) =>
            ctx.log.info("******************{} get value at {},{}",ctx.self.path,nodeAddress,entityContext.entityId)
            replyTo ! SubTtl(entityContext.entityId,value)
            Behaviors.same
          case Idle =>
            entityContext.shard ! ClusterSharding.Passivate(ctx.self)
            Behaviors.same
          case StopCounter =>
            Behaviors.stopped(() => ctx.log.info("************{} stopping ... passivated for idling.", entityContext.entityId))
        }
      }
      ctx.setReceiveTimeout(30.seconds, Idle)
      updated(0)
    }
  }
}
object EntityManager {
  sealed trait Command
  case class AddOne(counterId: String) extends Command
  case class GetSum(counterId: String ) extends Command
  case class WrappedTotal(res: Counter.Response) extends Command


  def apply(): Behavior[Command] = Behaviors.setup { ctx =>
    val cluster = Cluster(ctx.system)
    val sharding = ClusterSharding(ctx.system)
    val entityType = Entity(Counter.TypeKey) { entityContext =>
      Counter(cluster.selfMember.address.toString,entityContext)
    }.withStopMessage(Counter.StopCounter)
    sharding.init(entityType)

    val counterRef: ActorRef[Counter.Response] = ctx.messageAdapter(ref => WrappedTotal(ref))

     Behaviors.receiveMessage[Command] {
      case AddOne(cid) =>
        val entityRef: EntityRef[Counter.Command] = sharding.entityRefFor(Counter.TypeKey, cid)
        entityRef ! Counter.Increment
        Behaviors.same
      case GetSum(cid) =>
         val entityRef: EntityRef[Counter.Command] = sharding.entityRefFor(Counter.TypeKey, cid)
         entityRef ! Counter.GetValue(counterRef)
         Behaviors.same
      case WrappedTotal(ttl) => ttl match {
        case Counter.SubTtl(eid,subttl) =>
          ctx.log.info("***********************{} total: {} ",eid,subttl)
      }
      Behaviors.same
    }
  }

}

object ClusterShardingApp  {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty) {
      startup("shard", 25251)
      startup("shard", 25252)
      startup("shard", 25253)
      startup("front", 25254)
    } else {
      require(args.size == 2, "Usage: role port")
      startup(args(0), args(1).toInt)
    }
  }

  def startup(role: String, port: Int): Unit = {
    // Override the configuration of the port when specified as program argument
    val config = ConfigFactory
      .parseString(s"""
      akka.remote.artery.canonical.port=$port
      akka.cluster.roles = [$role]
      """)
      .withFallback(ConfigFactory.load("cluster"))

    val entityManager = ActorSystem[EntityManager.Command](EntityManager(), "ClusterSystem", config)
    if (role == "front") {
      entityManager ! EntityManager.AddOne("9013")
      entityManager ! EntityManager.AddOne("9014")
      entityManager ! EntityManager.AddOne("9013")
      entityManager ! EntityManager.AddOne("9015")
      entityManager ! EntityManager.AddOne("9013")
      entityManager ! EntityManager.AddOne("9014")
      entityManager ! EntityManager.AddOne("9014")
      entityManager ! EntityManager.AddOne("9013")
      entityManager ! EntityManager.AddOne("9015")
      entityManager ! EntityManager.AddOne("9015")
      entityManager ! EntityManager.AddOne("9016")
      entityManager ! EntityManager.GetSum("9014")
      entityManager ! EntityManager.GetSum("9015")
      entityManager ! EntityManager.GetSum("9013")
      entityManager ! EntityManager.GetSum("9016")
    }

  }

}

cluster.conf

akka {
  actor {
    provider = cluster

    serialization-bindings {
      "com.learn.akka.CborSerializable" = jackson-cbor
    }
  }
  remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 0
    }
  }
  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:25251",
      "akka://ClusterSystem@127.0.0.1:25252"]
  }
}

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Netty 中的內存分配淺析

Netty 出發點作為一款高性能的 RPC 框架必然涉及到頻繁的內存分配銷毀操作,如果是在堆上分配內存空間將會觸發頻繁的GC,JDK 在1.4之後提供的 NIO 也已經提供了直接直接分配堆外內存空間的能力,但是也僅僅是提供了基本的能力,創建、回收相關的功能和效率都很簡陋。基於此,在堆外內存使用方面,Netty 自己實現了一套創建、回收堆外內存池的相關功能。基於此我們一起來看一下 Netty 是如何實現內存分配的。

1. Netty 中的數據容器分類

談到數據保存肯定要說到內存分配,按照存儲空間來劃分,可以分為 堆內存 和 堆外內存;按照內存區域連貫性來劃分可以分為池化內存和非池化內存。這些劃分在 Netty 中的實現接口分別是:

按照底層存儲空間劃分:

  • 堆緩衝區:HeapBuffer;
  • 直接緩衝區:DirectBuffer。

按照是否池化劃分:

  • 池化:PooledBuffer;
  • 非池化:UnPooledBuffer。

默認使用 PoolDireBuf 類型的內存, 這些內存主要由 PoolArea 管理。另外 Netty 並不是直接對外暴露這些 API,提供了 Unsafe 類作為出口暴露數據分配的相關操作。

小知識:

什麼是池化?

一般申請內存是檢查當前內存哪裡有適合當前數據塊大小的空閑內存塊,如果有就將數據保存在當前內存塊中。

那麼池化想做的事情是:既然每次來數據都要去找內存地址來存,我就先申請一塊內存地址,這一塊就是我的專用空間,內存分配、回收我全權管理。

池化解決的問題:

內存碎片:

內碎片

內碎片就是申請的地址空間大於真正數據使用的內存空間。比如固定申請1M的空間作為某個線程的使用內存,但是該線程每次最多只佔用0.5M,那麼每次都有0.5M的碎片。如果該空間不被有效回收時間一長必然存在內存空洞。

外碎片

外碎片是指多個內存空間合併的時候發現不夠分配給待使用的空間大小。比如有一個 20byte,13byte 的連續內存空間可以被回收,現在有一個 48byte 的數據塊需要存儲,而這兩個加起來也只有 33byte 的空間,必然不會被使用到。

如何實現內存池?

  1. 鏈表維護空閑內存地址

    最簡單的就是弄一個鏈表來維護當前空閑的內存空間地址。如果有使用就從鏈表刪除,有釋放就加入鏈表對應位置。這種方式實現簡單,但是搜索和釋放內存維護的難度還是比較大,不太適合。

  2. 定長內存空間分配

    維護兩個列表,一個是未分配內存列表,一個是已分配內存列表。每個內存塊都是一樣大小,分配時如果不夠就將多個塊合併到一起。這種方式的缺點就是會浪費一定的內存空間,如果有特定的場景還是沒有問題。

  3. 多段定長池分配

    在上面的定長分配基礎上,由原來的固定一個長度分配空間變為按照不同對象大小(8,16,32,64,128,256,512,1k…64K),的方式分配多個固定大小的內存池。每次要申請內存的時候按照當前對象大小去對應的池中查找是否有剩餘空間。

    Linux 本身支持動態內存分配和釋放,對應的命令為:malloc/free。malloc 的全稱是 memory allocation,中文叫動態內存分配,用於申請一塊連續的指定大小的內存塊區域以void*類型返回分配的內存區域地址。

    malloc / free的實現過程:

    1. 空閑存儲空間以空閑鏈表的方式組織(地址遞增),每個塊包含一個長度、一個指向下一塊的指針以及一個指向自身存儲空間的指針。( 因為程序中的某些地方可能不通過 malloc 調用申請,因此 malloc 管理的空間不一定連續)
    2. 當有申請請求時,malloc 會掃描空閑鏈表,直到找到一個足夠大的塊為止(首次適應)(因此每次調用malloc 時並不是花費了完全相同的時間)
    3. 如果該塊恰好與請求的大小相符,則將其從鏈表中移走並返回給用戶。如果該塊太大,則將其分為兩部分,尾部的部分分給用戶,剩下的部分留在空閑鏈表中(更改頭部信息)。因此 malloc 分配的是一塊連續的內存。
    4. 釋放時首先搜索空閑鏈表,找到可以插入被釋放塊的合適位置。如果與被釋放塊相鄰的任一邊是一個空閑塊,則將這兩個塊合為一個更大的塊,以減少內存碎片。

2. Netty 中的內存分配

Netty 採用了 jemalloc 的思想,這是 FreeBSD 實現的一種併發 malloc 的算法。jemalloc 依賴多個 Arena(分配器) 來分配內存,運行中的應用都有固定數量的多個 Arena,默認的數量與處理器的個數有關。系統中有多個 Arena 的原因是由於各個線程進行內存分配時競爭不可避免,這可能會極大的影響內存分配的效率,為了緩解高併發時的線程競爭,Netty 允許使用者創建多個分配器(Arena)來分離鎖,提高內存分配效率。

線程首次分配/回收內存時,首先會為其分配一個固定的 Arena。線程選擇 Arena 時使用 round-robin 的方式,也就是順序輪流選取。

每個線程各種保存 Arena 和緩存池信息,這樣可以減少競爭並提高訪問效率。Arena 將內存分為很多 Chunk 進行管理,Chunk 內部保存 Page,以頁為單位申請。申請內存分配時,會將分配的規格分為幾類:TINY,SAMLL,NORMAL 和 HUGE,分別對應不同的範圍,處理過程也不相同。

tiny 代表了大小在 0-512B 的內存塊;

small 代表了大小在 512B-8K 的內存塊;

normal 代表了大小在 8K-16M 的內存塊;

huge 代表了大於 16M 的內存塊。

每個塊裏面又定義了更細粒度的單位來分配數據:

  • Chunk:一個 Chunk 的大小是 16M,Chunk 是 Netty 對操作系統進行內存申請的單位,後續所有的內存分配都是在 Chunk 裏面進行操作。
  • Page:Chunk 內部以 Page 為單位分配內存,一個 Page 大小為 8K。當我們需要 16K 的空間時,Netty 就會從一個 Chunk 中找到兩個 Page 進行分配。
  • Subpage 和 element:element 是比 Page 更小的單位,當我們申請小於 8K 的內存時,Netty 會以 element 為單位進行內存分配。element 沒有固定大小,具體由用戶的需求決定。Netty 通過 Subpage 管理 element,Subpage 是由 Page 轉變過來的。當我們需要 1K 的空間時,Netty 會把一個 Page 變成 Subpage,然後把 Subpage 分成 8 個 1K 的 element 進行分配。

Chunk 中的內存分配

線程分配內存主要從兩個地方分配: PoolThreadCache 和 Arena。其中 PoolThreadCache 線程獨享, Arena 為幾個線程共享。

初次申請內存的時候,Netty 會從一整塊內存(Chunk)中分出一部分來給用戶使用,這部分工作是由 Arena 來完成。而當用戶使用完畢釋放內存的時候,這些被分出來的內存會按不同規格大小放在 PoolThreadCache 中緩存起來。當下次要申請內存的時候,就會先從 PoolThreadCache 中找。

Chunk、Page、Subpage 和 element 都是 Arena 中的概念,Arena 的工作就是從一整塊內存中分出合適大小的內存塊。Arena 中最大的內存單位是 Chunk,這是 Netty 向操作系統申請內存的單位。而一塊 Chunk(16M) 申請下來之後,內部會被分成 2048 個 Page(8K),當用戶向 Netty 申請超過 8K 內存的時候,Netty 會以 Page 的形式分配內存。

Chunk 內部通過夥伴算法管理 Page,具體實現為一棵完全平衡二叉樹:

二叉樹中所有子節點管理的內存也屬於其父節點。當我們要申請大小為 16K 的內存時,我們會從根節點開始不斷尋找可用的節點,一直到第 10 層。那麼如何判斷一個節點是否可用呢?Netty 會在每個節點內部保存一個值,這個值代表這個節點之下的第幾層還存在未分配的節點。比如第 9 層的節點的值如果為 9,就代表這個節點本身到下面所有的子節點都未分配;如果第 9 層的節點的值為 10,代表它本身不可被分配,但第 10 層有子節點可以被分配;如果第 9 層的節點的值為 12,此時可分配節點的深度大於了總深度,代表這個節點及其下面的所有子節點都不可被分配。下圖描述了分配的過程:

對於小內存(小於4096)的分配還會將 Page 細化成更小的單位 Subpage。Subpage 按大小分有兩大類:

  1. Tiny:小於 512 的情況,最小空間為 16,對齊大小為 16,區間為[16,512),所以共有 32 種情況。
  2. Small:大於等於 512 的情況,總共有四種,512,1024,2048,4096。

PoolSubpage 中直接採用位圖管理空閑空間(因為不存在申請 k 個連續的空間),所以申請釋放非常簡單。

第一次申請小內存空間的時候,需要先申請一個空閑頁,然後將該頁轉成 PoolSubpage,再將該頁設為已被佔用,最後再把這個 PoolSubpage 存到 PoolSubpage 池中。這樣下次就不需要再去申請空閑頁了,直接去池中找就好了。Netty 中有 36 種 PoolSubpage,所以用 36 個 PoolSubpage 鏈表表示 PoolSubpage 池。

因為單個 PoolChunk 只有 16M,這遠遠不夠用,所以會很很多很多 PoolChunk,這些 PoolChunk 組成一個鏈表,然後用 PoolChunkList 持有這個鏈表。

我們先從內存分配器 PoolArena 來分析 Netty 中的內存是如何分配的,Area 的工作就是從一整塊內存中協調如何分配合適大小的內存給當前數據使用。PoolArena 是 Netty 的內存池實現抽象類,其內部子類為 HeapArena 和 DirectArena,HeapArena 對應堆內存(heap buffer),DirectArena 對應堆外直接內存(direct buffer),兩者除了操作的內存(byte[]和ByteBuffer)不同外其餘完全一致。

從結構上來看,PoolArena 中主要包含三部分子內存池:

tinySubpagePools;

smallSubpagePools;

一系列的 PoolChunkList。

tinySubpagePools 和 smallSubpagePools 都是 PoolSubpage 的數組,數組長度分別為 32 和 4。

PoolChunkList 則主要是一個容器,其內部可以保存一系列的 PoolChunk 對象,並且,Netty 會根據內存使用率的不同,將 PoolChunkList 分為不同等級的容器。

abstract class PoolArena<T> implements PoolArenaMetric {

   enum SizeClass {
        Tiny,
        Small,
        Normal
    }
  // 該參數指定了tinySubpagePools數組的長度,由於tinySubpagePools每一個元素的內存塊差值為16,
	// 因而數組長度是512/16,也即這裏的512 >>> 4
  static final int numTinySubpagePools = 512 >>> 4;
	//表示該PoolArena的allocator
  final PooledByteBufAllocator parent;
  //表示PoolChunk中由Page節點構成的二叉樹的最大高度,默認11
  private final int maxOrder;
  //page的大小,默認8K
  final int pageSize;
  // 指定了恭弘=叶 恭弘節點大小8KB是2的多少次冪,默認為13,該字段的主要作用是,在計算目標內存屬於二叉樹的
	// 第幾層的時候,可以藉助於其內存大小相對於pageShifts的差值,從而快速計算其所在層數
  final int pageShifts;
  //默認16MB
  final int chunkSize;
  // 由於PoolSubpage的大小為8KB=8196,因而該字段的值為
	// -8192=>=> 1111 1111 1111 1111 1110 0000 0000 0000
	// 這樣在判斷目標內存是否小於8KB時,只需要將目標內存與該数字進行與操作,只要操作結果等於0,
	// 就說明目標內存是小於8KB的,這樣就可以判斷其是應該首先在tinySubpagePools或smallSubpagePools
	// 中進行內存申請
  final int subpageOverflowMask;
  // 該參數指定了smallSubpagePools數組的長度,默認為4
  final int numSmallSubpagePools;
  //tinySubpagePools用來分配小於512 byte的Page
  private final PoolSubpage<T>[] tinySubpagePools;
  //smallSubpagePools用來分配大於等於512 byte且小於pageSize內存的Page
  private final PoolSubpage<T>[] smallSubpagePools;
  //用來存儲用來分配給大於等於pageSize大小內存的PoolChunk
  //存儲內存利用率50-100%的chunk
  private final PoolChunkList<T> q050;
  //存儲內存利用率25-75%的chunk
  private final PoolChunkList<T> q025;
  //存儲內存利用率1-50%的chunk
  private final PoolChunkList<T> q000;
  //存儲內存利用率0-25%的chunk
  private final PoolChunkList<T> qInit;
  //存儲內存利用率75-100%的chunk
  private final PoolChunkList<T> q075;
  //存儲內存利用率100%的chunk
  private final PoolChunkList<T> q100;
	//堆內存(heap buffer)
  static final class HeapArena extends PoolArena<byte[]> {
  
  }
   //堆外直接內存(direct buffer)
  static final class DirectArena extends PoolArena<ByteBuffer> {
    
  }
  
  
}

如上所示,PoolArena 是由多個 PoolChunk 組成的大塊內存區域,而每個 PoolChun k則由多個 Page 組成。當需要分配的內存小於 Page 的時候,為了節約內存採用 PoolSubpage 實現小於 Page 大小內存的分配。在PoolArena 中為了保證 PoolChunk 空間的最大利用化,按照 PoolArena 中各 個PoolChunk 已使用的空間大小將其劃分為 6 類:

  1. qInit:存儲內存利用率 0-25% 的 chunk;
  2. q000:存儲內存利用率 1-50% 的 chunk;
  3. q025:存儲內存利用率 25-75% 的 chunk;
  4. q050:存儲內存利用率 50-100% 的 chunk;
  5. q075:存儲內存利用率 75-100%的 chunk;
  6. q100:存儲內存利用率 100%的 chunk。

PoolArena 維護了一個 PoolChunkList 組成的雙向鏈表,每個 PoolChunkList 內部維護了一個 PoolChunk 雙向鏈表。分配內存時,PoolArena 通過在 PoolChunkList 找到一個合適的 PoolChunk,然後從 PoolChunk 中分配一塊內存。

下面來看 PoolArena 是如何分配內存的:

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
  // 將需要申請的容量格式為 2^N
  final int normCapacity = normalizeCapacity(reqCapacity);
  // 判斷目標容量是否小於8KB,小於8KB則使用tiny或small的方式申請內存
  if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
    int tableIdx;
    PoolSubpage<T>[] table;
    boolean tiny = isTiny(normCapacity);
    // 判斷目標容量是否小於512字節,小於512字節的為tiny類型的
    if (tiny) { // < 512
      // 將分配區域轉移到 tinySubpagePools 中
      if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
        // was able to allocate out of the cache so move on
        return;
      }
      // 如果無法從當前線程緩存中申請到內存,則嘗試從tinySubpagePools中申請,這裏tinyIdx()方法
      // 就是計算目標內存是在tinySubpagePools數組中的第幾號元素中的
      tableIdx = tinyIdx(normCapacity);
      table = tinySubpagePools;
    } else {
      // 如果目標內存在512byte~8KB之間,則嘗試從smallSubpagePools中申請內存。這裏首先從
      // 當前線程的緩存中申請small級別的內存,如果申請到了,則直接返回
      if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
        // was able to allocate out of the cache so move on
        return;
      }
      tableIdx = smallIdx(normCapacity);
      table = smallSubpagePools;
    }
		// 獲取目標元素的頭結點
    final PoolSubpage<T> head = table[tableIdx];

    // 這裏需要注意的是,由於對head進行了加鎖,而在同步代碼塊中判斷了s != head,
    // 也就是說PoolSubpage鏈表中是存在未使用的PoolSubpage的,因為如果該節點已經用完了,
    // 其是會被移除當前鏈表的。也就是說只要s != head,那麼這裏的allocate()方法
    // 就一定能夠申請到所需要的內存塊
    synchronized (head) {
      // s != head就證明當前PoolSubpage鏈表中存在可用的PoolSubpage,並且一定能夠申請到內存,
      // 因為已經耗盡的PoolSubpage是會從鏈表中移除的
      final PoolSubpage<T> s = head.next;
      // 如果此時 subpage 已經被分配過內存了執行下文,如果只是初始化過,則跳過該分支
      if (s != head) {
        // 從PoolSubpage中申請內存
        assert s.doNotDestroy && s.elemSize == normCapacity;
        // 通過申請的內存對ByteBuf進行初始化
        long handle = s.allocate();
        assert handle >= 0;
        // 初始化 PoolByteBuf 說明其位置被分配到該區域,但此時尚未分配內存
        s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
				// 對tiny類型的申請數進行更新
        if (tiny) {
          allocationsTiny.increment();
        } else {
          allocationsSmall.increment();
        }
        return;
      }
    }
    // 走到這裏,說明目標PoolSubpage鏈表中無法申請到目標內存塊,因而就嘗試從PoolChunk中申請
    allocateNormal(buf, reqCapacity, normCapacity);
    return;
  }
   // 走到這裏說明目標內存是大於8KB的,那麼就判斷目標內存是否大於16M,如果大於16M,
  // 則不使用內存池對其進行管理,如果小於16M,則到PoolChunkList中進行內存申請
  if (normCapacity <= chunkSize) {
    // 小於16M,首先到當前線程的緩存中申請,如果申請到了則直接返回,如果沒有申請到,
    // 則到PoolChunkList中進行申請
    if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
      // was able to allocate out of the cache so move on
      return;
    }
    allocateNormal(buf, reqCapacity, normCapacity);
  } else {
    // 對於大於16M的內存,Netty不會對其進行維護,而是直接申請,然後返回給用戶使用
    allocateHuge(buf, reqCapacity);
  }
}

所有內存分配的 size 都會經過 normalizeCapacity() 進行處理,申請的容量總是會被格式為 2^N。主要規則如下:

  1. 如果目標容量小於 16 字節,則返回 16;
  2. 如果目標容量大於 16 字節,小於 512 字節,則以 16 字節為單位,返回大於目標字節數的第一個 16 字節的倍數。比如申請的 100 字節,那麼大於 100 的 16 整數倍最低為: 16 * 7 = 112,因而返回 112;
  3. 如果目標容量大於 512 字節,則返回大於目標容量的第一個 2 的指數冪。比如申請的 1000 字節,那麼返回的將是:2^10 = 1024。

PoolArena 提供了兩種方式進行內存分配:

  1. PoolSubpage 用於分配小於 8k 的內存

    • tinySubpagePools:用於分配小於 512 字節的內存,默認長度為 32,因為內存分配最小為 16,每次增加16,直到512,區間[16,512)一共有 32 個不同值;
    • smallSubpagePools:用於分配大於等於 512 字節的內存,默認長度為 4;
  • tinySubpagePools 和 smallSubpagePools 中的元素默認都是 subpage。
  1. poolChunkList 用於分配大於 8k 的內存

    上面已經解釋了 q 開頭的幾個變量用於保存大於 8k 的數據。

默認先嘗試從 poolThreadCache 中分配內存,PoolThreadCache 利用 ThreadLocal 的特性,消除了多線程競爭,提高內存分配效率;

首次分配時,poolThreadCache 中並沒有可用內存進行分配,當上一次分配的內存使用完並釋放時,會將其加入到 poolThreadCache 中,提供該線程下次申請時使用。

如果是分配小內存,則嘗試從 tinySubpagePools 或 smallSubpagePools 中分配內存,如果沒有合適 subpage,則採用方法 allocateNormal 分配內存。

如果分配一個 page 以上的內存,直接採用方法 allocateNormal() 分配內存,allocateNormal()則會將申請動作交由 PoolChunkList 進行。

private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
  //如果在對應的PoolChunkList能申請到內存,則返回
  if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
      q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
      q075.allocate(buf, reqCapacity, normCapacity)) {
    ++allocationsNormal;
    return;
  }

  // Add a new chunk.
  PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
  long handle = c.allocate(normCapacity);
  ++allocationsNormal;
  assert handle > 0;
  c.initBuf(buf, handle, reqCapacity);
  qInit.add(c);
}

首先將申請動作按照 q050->q025->q000->qInit->q075 的順序依次交由各個 PoolChunkList 進行處理,如果在對應的 PoolChunkList 中申請到了內存,則直接返回。

如果申請不到,那麼直接創建一個新的 PoolChunk,然後在該 PoolChunk 中申請目標內存,最後將該 PoolChunk 添加到 qInit 中。

上面說過 Chunk 是 Netty 向操作系統申請內存塊的最大單位,每個 Chunk 是16M,PoolChunk 內部通過 memoryMap 數組維護了一顆完全平衡二叉樹作為管理底層內存分佈及回收的標記位,所有的子節點管理的內存也屬於其父節點。

關於 PoolChunk 內部如何維護完全平衡二叉樹就不在這裏展開,大家有興趣可以自行看源碼。

對於內存的釋放,PoolArena 主要是分為兩種情況,即池化和非池化,如果是非池化,則會直接銷毀目標內存塊,如果是池化的,則會將其添加到當前線程的緩存中。如下是 free()方法的源碼:

public void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity,
     PoolThreadCache cache) {
  // 如果是非池化的,則直接銷毀目標內存塊,並且更新相關的數據
  if (chunk.unpooled) {
    int size = chunk.chunkSize();
    destroyChunk(chunk);
    activeBytesHuge.add(-size);
    deallocationsHuge.increment();
  } else {
    // 如果是池化的,首先判斷其是哪種類型的,即tiny,small或者normal,
    // 然後將其交由當前線程的緩存進行處理,如果添加成功,則直接返回
    SizeClass sizeClass = sizeClass(normCapacity);
    if (cache != null && cache.add(this, chunk, nioBuffer, handle,
          normCapacity, sizeClass)) {
      return;
    }

    // 如果當前線程的緩存已滿,則將目標內存塊返還給公共內存塊進行處理
    freeChunk(chunk, handle, sizeClass, nioBuffer);
  }
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

分享我在前後端分離項目中Gitlab-CI的經驗

長話短說,今天分享我為前後端分離項目搭建Gitlab CI/CD流程的一些額外經驗。

Before

Gitlab-ci是Gitlab提供的CI/CD特性,結合Gitlab簡單友好的配置界面,能愉悅的在Gitlab界面查看管道執行流程,並自然流暢的推動敏捷開發流程。
Gitlab-CI/CD的核心是搭建Gitlab Runner、編寫.gitlab-ci.yaml文件。
詳細示例請參考:Gitlab CI/CD+ASP.NETCore.

本次前後端兩個項目使用同一個Gitlab Runner(shell模式),前端項目的gitlab-ci.yaml構建Job如圖:

Round 1

單個Gitlab Runner可為多個項目提供構建服務,

gitlab-Runner register命令只能接受一個註冊token,當時為支持多個項目,花了不少冤枉心思倒騰Gitlab Runner.

你可以為註冊的項目解鎖Runner,這樣Girlab Runner就可以為其他項目提供構建:

Round 2

使用Runner緩存加快前端構建過程
大家都知道npm_module被前端開發者詬病為毒瘤, 而Gitlab runner執行每次構建job之前都會清場,pull/fetch指定的代碼再執行job, 這就導致每次build job會耗時很久(要拉取毒瘤)。

#!/bin/bash

cd   packages/event-analysis
yarn config set registry http://registry.npm.gridsum.com &&  yarn --prefer-offline --frozen-lockfile
npm run build

以上是build任務的腳本frontend.sh,總耗時3m33s,其中yarn命令拉取npm_modules耗時172.52s

gitlab runner支持緩存
在.gitlab-ci.yaml 文件中定義cache指令:
cache被用來在job之間緩存文件,更強大的是可以定義文件依賴緩存:

build:
  stage: build
  cache:
    key:
      files:
        - packages/event-analysis/package.json
    paths:
      - node_modules
  script: 
    - ./frontend.sh
  tags:
    - my-tag

緩存key是yarn命令要用到的package.json,緩存內容是npm_modules;
只要這個package.json文件未變更,後續任務就會使用緩存的npm_modules,而不用重建npm_modules依賴。

使用runner緩存優化后build任務總耗時1m18s,其中yarn命令耗時22.83s:

以上針對Gitlab-CI的使用經驗點到為止,足夠應對我當前項目,更多請關注:

Reference

  1. https://docs.gitlab.com/ee/ci/runners/#prevent-a-specific-runner-from-being-enabled-for-other-projects
  2. https://docs.gitlab.com/ee/ci/caching/

Devops的圈子很大,上面的Gitlab-ci也只是點到為止,應付我當前的前後端分離項目.. 歡迎大家來捶我。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

博客園自用主題美化 – Light

事件背景

 

之前做過幾個主題,但是都有些看膩了,然後就想着啥時候重構一下,於是就有了現在的新樣式:

https://github.com/KU4NG/CNBLOG-Theme-Light

當然,如果你對之前的感興趣,也可以查看之前的分享:

https://github.com/KU4NG/CNBlog-Theme

裡面包含幾個主題,但是我是有些懶得維護了,太忙了,感興趣的自己去改改 CSS 就行了!

 

 

新主題配置方法

 

1. 基礎配置:

前往博客園設置頁面,就行配置,需要注意的是:

a. 博客標題就是最終你博客的 logo,所以選你喜歡的。

b. 我博客標題中的來自 windows 10 系統輸入法旁邊的表情。

c. 基礎主題選:SimpleMemory,這一步很重要,因為我得樣式都是根據該主題的 HTML 結構寫的。

 

2. 配置樣式:

將本文中提供的 CSS 代碼或者 github 上面 css 目錄下 style.css 文件中的內容粘貼到此處。

*{margin:0;padding:0;font-weight:normal;letter-spacing:1.5px;font-family:PingFangSC-Regular,HelveticaNeue-Light,'Helvetica Neue Light','Microsoft YaHei',sans-serif,Simsun}div{border-radius:0!important}a{text-decoration:none!important}a:link{text-decoration:none!important}a:visited{text-decoration:none!important}a:hover{text-decoration:none!important}a:active{text-decoration:none!important}body{background-color:#eee}#header{min-width:1200px;height:50px;width:100%;display:block;background-color:white;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}#header *{color:#333!important}#header h1{float:left;width:200px;padding-left:20px}#header h1 a{height:46px;line-height:46px;font-size:18px!important;font-weight:bolder!important;letter-spacing:3px!important}#header #lnkBlogLogo{display:none}#header h2{display:none}#header li{list-style:none;float:right;height:50px;line-height:50px;position:relative}#header li:hover{cursor:pointer}#header li a{font-size:12px;letter-spacing:2px;padding:0 20px}#header li a::before,#header li a::after{position:absolute;top:0;right:0;bottom:0;left:0;content:'';opacity:0;pointer-events:none;-webkit-transition:opacity .4s,-webkit-transform .4s;transition:opacity .4s,transform .4s}#header li a::before{border-top:2px solid #333;-webkit-transform:scale(0,1);transform:scale(0,1)}#header li:hover a::before,#header li:hover a::after{opacity:1;-webkit-transform:scale(1);transform:scale(1)}#header .blogStats{display:none}#header #navigator{margin-right:200px}#sidebar_search{display:block;width:200px;position:absolute;top:0;right:0;height:48px;line-height:48px}#sidebar_search h3{display:none}#sidebar_search .div_my_zzk{margin-top:0;margin-bottom:0}#sidebar_search .input_my_zzk{display:inline-block;vertical-align:middle;padding:0 10px;border:0;cursor:text;font-size:12px}#sidebar_search .btn_my_zzk{background-color:#009688;color:#fff!important;white-space:nowrap;text-align:center;cursor:pointer;padding-left:10px;padding-right:10px}#sidebar_search input:focus{outline:0}#sidebar_search input{outline:0;border:0}#main{width:1200px;margin:10px auto}#main #mainContent{width:960px;background-color:white;padding:20px;float:left}#main #mainContent .day{position:relative;border:1px dashed lightgrey;padding:15px;margin:0 0 15px 0}#main #mainContent .day .dayTitle{position:absolute;top:-1px;right:-1px;background-color:#eee;width:110px;text-align:right;padding:0 10px 3px 10px}#main #mainContent .day .dayTitle a{font-size:12px;color:#333;opacity:.6}#main #mainContent .day .postTitle a{position:relative;left:-18px;border-left:4px solid #dc3545;padding-left:20px;font-size:18px;font-weight:bolder!important;color:#333}#main .postTitle span{font-weight:bolder!important}#main #mainContent .day .postTitle a:hover{color:#036}#main #mainContent .day .postCon{padding:10px}#main #mainContent .day .postCon .c_b_p_desc{font-size:12px;letter-spacing:2px;opacity:.6;line-height:2;width:900px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#main #mainContent .day .postDesc{font-size:12px;opacity:.6;text-align:right}#main #mainContent .day .postDesc a{font-size:12px;color:#dc3545}#main #mainContent .day .postSeparator{height:15px;border-top:1px dashed lightgrey;border-bottom:1px dashed lightgrey;border-left:2px solid white;border-right:2px solid white;margin:15px -17px}#main #mainContent #nav_next_page a{font-size:12px;color:#333}#main #mainContent .topicListFooter{margin-right:0}#main #mainContent .pager{font-size:12px;color:#dc3545;text-align:right}#main #mainContent .pager a{font-size:12px;color:gray;border:1px solid lightgray}#main #mainContent #homepage_top_pager{display:none}#sideBar{width:200px;float:right}#sideBar #sideBarMain{width:190px;float:right}#sideBar #sideBarMain *{color:#333;font-size:12px;letter-spacing:2px}#sideBar #sideBarMain #sidebar_news{background-color:white}#sideBar #sideBarMain #sidebar_news h3{background-color:#dc3545;color:white;border-bottom:1px solid #eee;padding:5px 15px;font-size:14px}#sideBar #sideBarMain #sidebar_news #blog-news{padding:15px 15px 0 15px}#sideBar #sideBarMain #sidebar_news #blog-news *{line-height:25px}#sideBar #sideBarMain #sidebar_news #blog-news #profile_block{margin-top:0}#sideBar #sideBarMain #sidebar_categories{background-color:white}#sideBar #sideBarMain #sidebar_categories h3{padding:20px 15px 5px 15px}#sideBar #sideBarMain #sidebar_categories ul{list-style:none;counter-reset:headings;padding:0 15px 15px 15px}#sideBar #sideBarMain #sidebar_categories ul li{line-height:30px;border-bottom:1px dashed #eee}#sideBar #sideBarMain #sidebar_categories ul li:before{counter-increment:headings;content:counter(headings,decimal) ".";font-family:"Bree Serif",serif}#sideBar #sideBarMain #sidebar_categories ul li a{letter-spacing:1px}#sideBar #sideBarMain #sidebar_categories ul li a:hover{color:#dc3545}#footer{font-size:12px;text-align:center;line-height:25px;margin-top:20px;margin-bottom:20px;color:#036}
#footer br{display:none}#main #mainContent .entrylist h1{font-size:18px;font-weight:bolder;text-align:center;margin-top:15px;margin-bottom:30px}#main #mainContent .entrylistItem{position:relative;border:1px dashed lightgrey;padding:15px;margin:0 0 15px 0}#main #mainContent .entrylistItem .entrylistPosttitle a{position:relative;left:-18px;border-left:4px solid #dc3545;padding-left:20px;font-size:18px;font-weight:bolder;color:#333}#main #mainContent .entrylistItem .entrylistPosttitle a:hover{color:#036}#main .entrylistPosttitle span{font-weight:bolder}#main #mainContent .entrylistItem .entrylistPostSummary{padding:10px}#main #mainContent .entrylistItem .entrylistPostSummary .c_b_p_desc{font-size:12px;letter-spacing:2px;opacity:.6;line-height:2;width:900px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#main #mainContent .entrylistItem .entrylistItemPostDesc{font-size:12px;opacity:.6;text-align:right}#main #mainContent .entrylistItem .entrylistItemPostDesc a{font-size:12px;color:#dc3545}#main #post_detail{padding:30px;position:relative}#main #post_detail .postTitle{padding:0 0 50px 0;text-align:center;border-bottom:1px dotted #ccc}#main #post_detail .postTitle a{font-size:20px;color:#333;font-weight:bolder}#main #post_detail .postDesc{position:absolute;width:calc(100% - 60px);top:85px;text-align:center}#main #post_detail .postDesc{font-size:12px;line-height:25px;color:#333;opacity:.7}#main #post_detail .postDesc *{font-size:12px;color:#333;opacity:.7}#main #post_detail .postBody #cnblogs_post_body{margin-top:30px;padding:15px}#main #post_detail .postBody #cnblogs_post_body p,#main #post_detail .postBody #cnblogs_post_body span{letter-spacing:1.5px;line-height:30px;font-size:14px;font-family:SimHei;margin:0;opacity:.9}#main #post_detail .postBody #cnblogs_post_body img{margin:15px 0;max-width:880px}#main #post_detail .postBody #cnblogs_post_body table{width:100%;margin:15px 0}#main #post_detail .postBody #cnblogs_post_body table *{font-size:12px}#main #post_detail .postBody #cnblogs_post_body table th{padding:6px 10px!important;text-align:left;background-color:#1c2b36;color:white;border-color:#1c2b36}#main #post_detail .postBody #cnblogs_post_body table td{padding:6px 10px!important;text-align:left}#main #post_detail .postBody #cnblogs_post_body .cnblogs_code{padding:15px 20px;border:0}#main #post_detail .postBody #cnblogs_post_body .cnblogs_code_toolbar{display:none}#main #post_detail .postBody #cnblogs_post_body blockquote{border:0;background-color:#eee;border-left:5px solid #009688}#main #post_detail .postBody #cnblogs_post_body blockquote *{font-size:12px;color:#333}#main #post_detail .postBody #blog_post_info_block{padding:15px}#main #post_detail .postBody #blog_post_info_block *{font-size:12px;color:#333}#main #post_detail .postBody #blog_post_info_block #author_profile_info{display:none}#main #post_detail .postBody #blog_post_info_block #author_profile_detail{display:none}#main #post_detail .postBody #blog_post_info_block #blog_post_info #green_channel{float:left;border:0}#main #post_detail .postBody #blog_post_info_block #blog_post_info #div_digg{float:right}#main #comment_form{padding:45px!important}#main #comment_form *{font-size:12px}#main #comment_form .comment_textarea{width:100%}#main #blog-comments-placeholder{padding:0 45px!important}#main #blog-comments-placeholder *{font-size:12px;color:#333}#main #blog-comments-placeholder .feedbackItem{padding:15px 0;border-bottom:1px dashed #eee}#main #blog-comments-placeholder .feedbackListSubtitle{line-height:30px}#main #blog-comments-placeholder a{color:#009688}#main #blog-comments-placeholder div{line-height:30px}#main #blog-comments-placeholder .feedbackManage{float:right}#main #blog-comments-placeholder .feedbackListSubtitle .layer{color:#036;font-weight:bolder}#main #blog-comments-placeholder .feedbackListSubtitle .louzhu{color:white;background-color:#1c2b36;padding:0 10px;margin:0 -8px}#main #blog-comments-placeholder br{display:none;padding-left:20px}#main #commentbox_opt #btn_comment_submit{background-color:#009688;color:#fff!important;white-space:nowrap;text-align:center;cursor:pointer;padding-left:10px;padding-right:10px;border:0;width:auto}#main #commentbox_opt a{color:#dc3545}.my-title{font-size:18px!important;font-weight:bolder;font-family:simsun!important;border-left:5px solid #dc3545;padding-left:10px;line-height:20px!important;position:relative;left:-15px}#comment_nav{display:none}#ad_t2{display:none}.c_ad_block{display:none}#under_post_news{display:none}

注意,一定要選擇禁用默認的樣式,否則可能樣式衝突! 

 

3. 配置側邊欄:

代碼如下:

<div style="width: 100%;text-align: center;">
    <img style="border-radius: 50%;border: 1px solid #eee;" src="https://pic.cnblogs.com/face/979767/20180915094029.png" alt="">
</div>
<div>聯繫: Q-1214966109</div>

值得注意的是:

a. 頭像的連接可以在自己的博客中找到,如: 

b. 聯繫方式可以寫自己,如果你還需要其它項目,繼續添加 div 即可!

最終的效果就是我的效果:

 

4. 配置首頁按鈕和 GITHUB 鏈接:

代碼如下:

<script>
    // 菜單新加標籤
    var indexEle = '<li><a target="_blank" class="menu" href="https://www.cnblogs.com/Dy1an">首頁</a></li>';
    var githubEle = '<li><a target="_blank" class="menu" href="https://github.com/KU4NG">GITHUB</a></li><li><a target="_blank" class="menu" href="https://github.com/KU4NG">主題</a></li>';
    document.getElementById('navList').insertAdjacentHTML("beforeEnd", indexEle);
    document.getElementById('navList').insertAdjacentHTML("afterBegin", githubEle);
</script>

需要注意的是:

a. 首頁鏈接記得換成自己的,當然你要是直接用我的我也是不會介意的。

b. GITHUB 你可以選擇換成其它的自己的一些資源。

c. 主題鏈接建議不要去掉,這裏吸波粉,讓我看到到底有多少人是支持這個項目的!

最終效果如下:

 

5. 配置搜索框:

這裏沒有需要修改的,直接粘貼就行,代碼如下:

<script>
    window.onload =  function() {
        var ele = document.getElementById('q');
        console.log(ele);
        ele.setAttribute("placeholder","搜索相關博客...");
        ele.setAttribute("autocomplete","off");
    }
</script>

最終效果:

已知問題:

由於網絡原因可能導致沒有加載或者加載不完全,刷新頁面即可!

 

6. 配置菜單:

想要以我的博客效果显示,則需要配置菜單:

在選項配置中選擇配置!

 

 

特殊用法說明 

 

1. 段落標題:

文中出現段落標題需要瘦動修改 HTML:

增加樣式:

代碼:

class="my-title"

 

2. 表格:

表格同樣需要以 html 格式插入:

<table>
    <thead>
        <th>
            <td>標題</td>
            <td>標題</td>
            <td>標題</td>
        </th>
    </thead>
    <tbody>
        <tr>
            <td>數據行</td>
            <td>數據行</td>
            <td>數據行</td>
        </tr>
        <tr>
            <td>數據行</td>
            <td>數據行</td>
            <td>數據行</td>
        </tr>
    </tbody>
</table>

效果如下:

 

3. 圖片:

最後就是截圖,文中的截圖建議大小不超過 870px,865px最佳,否則圖片會壓縮,影響體驗!

 

 

意見和建議

 

最後就是幾點說明:

1. 如果遇到樣式問題,歡迎反饋,QQ,留言,GITHUB 都可以。

2. 如果覺得可以,可以去 GITHUB star 支持一下,也可以在本文中頂一下! 

3. 最後吹一波博客園!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

docker registry 鏡像同步

docker registry 鏡像同步

Intro

之前我們的 docker 鏡像是保存在 Azure 的 Container Registry 里的,最近我們自己搭建了一個 docker registry,我們想把之前保存的 Azure 的 Container Registry 的 docker 鏡像同步到我們自己的 docker registry 里

實現思路

我們的做法比較簡單也比較LOW,但是基本可以滿足要求,

我們的做法是

  1. 首先獲取到源 Registry 里的所有鏡像列表
  2. 然後逐個獲取鏡像的 tags
  3. 然後依次遍歷將對應的鏡像拉到本地,然後 docker tag 一下,命名為新的 registry 鏡像名稱
  4. 然後 push docker 鏡像到新的 registry
  5. 刪除下載到本地的鏡像和推送到新的 registry 的鏡像

後來突然想起來阿里雲好像有一個鏡像同步工具,https://github.com/AliyunContainerService/image-syncer image-syncer 是一個docker鏡像同步工具,可用來進行多對多的鏡像倉庫同步,支持目前絕大多數主流的docker鏡像倉庫服務,看介紹還是很棒的,有需要 registry 之間同步鏡像的可以試試這個工具,看介紹這個工具不會拉取到本地磁盤,從源 registry 獲取鏡像數據之後直接就推送到新的 registry 里了,效率會高很多

Docker-Registry API

docker registry 有一套規範,可以查閱 https://docs.docker.com/registry/spec/api/ 了解更多

獲取所有鏡像

docker registry v2 新增了一個 _catalog 的 api 可以獲取所有的鏡像,v1 可以用 _search 來代替

語法如下:

GET /v2/_catalog

默認最多返回100條記錄,多餘 100 可以通過參數 n 指定返回數量,分頁的話可以指定另外一個參數 last指定完上一頁返回的最後一個鏡像,舉個栗子: http://example.com/v2/_catalog?n=20&last=b

獲取鏡像的 tag

獲取 docker 鏡像的 tag 列表可以使用 GET /v2/<repository-name>/tags/list 來獲取,也可以分頁,類似於上面獲取鏡像列表,可以通過 n 和 last 來實現分頁加載

操作示例

在本地部署了一個測試用的 docker registry 來做演示,我這裏用 httpie 來做測試

獲取鏡像列表:

調用 _catalog 接口來獲取鏡像列表

http :5000/v2/_catalog

獲取鏡像的 tag 列表

調用 tags/list 接口獲取鏡像的 tag

http :5000/v2/busybox/tags/list
http :5000/v2/redis/tags/list

PowerShell 腳本

一切不是自動化的運維都是耍流氓,很有可能以後會有類似的需求,不如寫個腳本自動化的跑吧

下面的腳本做了一些簡化,因為我們的 azure container registry 上的數量不多,只有五六十個鏡像,而且鏡像只有 latest 的 tag,沒有其他 tag ,所以把上面的步驟做了簡化,並沒有分頁獲取所有的鏡像,也沒有獲取所有的 tag,實際使用的話還請自行修改后使用

# variables
$srcRegUser = "xxx"
$srcRegPwd = "111111"
$srcRegHost = "xxx.azurecr.cn"
$destRegUser = "yyy"
$destRegPwd = "222"
$destRegHost = "registry.xxx.com"


# get repositories from source registry
# httpie
$response = (http -b -a "${srcRegUser}:${srcRegPwd}" "https://${srcRegHost}/v2/_catalog") | ConvertFrom-Json
# curl
#$response = (curl -u "${srcRegUser}:${srcRegPwd}" "https://${srcRegHost}/v2/_catalog") | ConvertFrom-Json
# repository
$repositories = $response.repositories

#
Write-Host $repositories

# login source registry
docker login $srcRegHost -u $srcRegUser -p $srcRegPwd
# login dest registry
docker login $destRegHost -u $destRegUser -p $destRegPwd

# sync
foreach($repo in $repositories)
{
    Write-Host "sync $repo begin"

    $srcTag = "${srcRegHost}/${repo}:latest"
    $destTag = "${destRegHost}/${repo}:latest"

    Write-Host "source image tag: $srcTag"
    Write-Host "dest image tag $destTag"

    Write-Host "docker pull $srcTag begin"

    docker pull $srcTag

    Write-Host "docker pull $srcTag completed"

    Write-Host "docker tag $srcTag $destTag ing"

    docker tag $srcTag $destTag

    Write-Host "docker push $destTag begin"

    docker push $destTag

    Write-Host "docker push $destTag completed"
    
    Write-Host "docker rmi $srcTag $destTag begin"

    docker rmi $srcTag $destTag

    Write-Host "docker rmi $srcTag $destTag end"

    Write-Host "sync $repo completed"
}

Write-Host "Completed..."

More

如果要同步的鏡像比較多,考慮使用阿里雲的鏡像同步工具去同步

Reference

  • https://stackoverflow.com/questions/31251356/how-to-get-a-list-of-images-on-docker-registry-v2
  • https://github.com/AliyunContainerService/image-syncer
  • https://docs.docker.com/registry/spec/api/

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

復古貝雷帽優雅來襲,自帶摩登時尚感!

風大的季節,外出的時候,要是沒有帽子,真的是太對不起自己努力梳理的髮型了。所以,在復古風潮“泛濫”的時機中,貝雷帽優雅來襲,自帶摩登時尚感,你是否心動了呢?

而對於貝雷帽,相信大家也不陌生,能為氣質加分,又能輕鬆顯臉小,各種優點應有盡有,那麼要是心動了的話,就一起來look一下吧。

不管你的情緒有多暴躁,但是一遇到耐心又溫柔的人,相信你也會逐漸被感化的。而性感又優雅的一字肩襯衫,配上高腰的黑色短裙,很是撩人。而且這身搭配,相信會在小個子當中瞬間流行起來。再來一頂黑色的貝雷帽,配上墨鏡,顯出神秘的氣息,但是那柔美的氣質,真的是無法抵擋。

條紋衫的出現,讓復古風潮變得更加濃烈,經典的黑白配色相結合,自帶摩登時尚感。當黑色貝雷帽遇上黑色牛仔褲,同色系遙相呼應,顯得造型協調又耐看。腳絲絨材質的短靴,貴族氣息迎面襲來,讓你的顏值輕鬆爆表。

雙色系穿搭,看着很簡約,卻很有時髦度。一身黑色的長外套,志玲姐姐的高挑身材輕鬆駕馭。內搭白色單品,不規則的袖子將白色單品秀了出來,演繹着經典的“天仙配”。復古風潮“泛濫”,一頂白色的貝雷帽,瞬間點亮造型,氣質指數更上一層樓。

襯衫在秋季單品中,是最受歡迎的,不僅百搭還顯氣質。而且隨着時尚的發展,襯衫的版型也在不斷地成長,變得更加新穎。比如這件格紋襯衫,斜肩的設計,還有不規則的剪裁,處處細節都在顯出滿分的時尚氣息。而如此氣質的襯衫,加上紅色的貝雷帽優雅來襲,更是吸睛無數。

黑色用在貝雷帽上,將典雅美髮揮得淋漓盡致。秋風陣陣,不經意間颳起了復古風,所以在復古風潮“泛濫”中,身上穿格紋的中長款外套,讓你隨時隨地跟上時尚潮流。配上同色系的皮質短裙,增添造型的質感,想不時髦都難。

要是覺得黑色太低調了,那麼淺色的貝雷帽與黑色的頭髮就能碰撞出“火花”,自帶摩登時尚感,相信值得小仙女get一下的。而深色的風衣配白色的條紋褲,顯得造型感很足,能隨時隨地展現你的時尚魅力。

而相對前面的穿搭來說,這身look就比較簡約一些,黑色貝雷帽優雅來襲,配灰色針織衫,搭小黑褲和黑色短靴,簡單而不平凡。而戴上墨鏡,給人帶來一副“墨鏡一戴誰都不愛的感覺”。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※你應該要知道的電子煙懶人包!

※男人為什麼總愛舒壓按摩台北外送茶交流呢?

※老司機曝包養內幕

※那些年被包養的回憶實錄

9套穿搭範本,舒適又休閑

我想,不管是人還是物,最講究的一個就是舒適感。而我們的穿搭更是如此,所以我要為小仙女們安利這9套穿搭範本,舒適又休閑,讓你輕輕鬆松美上一整秋。

第一套:

格紋襯衫是襯衫界的代表,出鏡率那是相當的高。深色的格紋襯衫,搭配顯瘦的小黑褲,盡顯幹練與十足的魅力。還將襯衫扎進褲子里,展示出腰以下都是腿的既視感。要是覺得造型太沉悶了,那麼在挑選包包的時候,記得pick這種白色調的,能起到點亮look的作用。

第二套:

而純色的襯衫也是很不錯的,清爽的藍色,讓你輕鬆延續夏季的時尚。是這9套穿搭範本中,我最喜歡的造型。照樣是配黑色的牛仔褲,褲腳採用翻邊的穿搭方式,秀出滿分的時髦度。一雙紅色的平底鞋,輕鬆奪人眼球,吸睛度爆棚。

第三套:

接下來的天氣會越來越冷,所以在覺得自己單穿襯衫hold不住冷空氣的時候,記得用疊穿的方式,外搭一件衛衣,舒適又休閑。就像這樣,衛衣與襯衫的顏色是深淺不一的色調,顯出強大的視覺衝擊感。

第四套:

或者疊穿一件外套也是不錯的選擇,這種白色外套雖然不耐臟,但是很百搭,也很顯氣質,基本上是人手必備一件。高級感十足的灰色休閑褲,讓你美上一整秋,展現你的十足魅力。

第五套:

平時休息的時候,小仙女還是選擇一些比較休閑的單品。小仙女可以在裏面套用襯衫或者是T恤,女人味瞬間就倍增,是這9套穿搭範本中,最能代表秋季最潮范的混搭風。

第六套:

深藍色的針織衫,小仙女千萬別錯過啦。而搭配米白色的休閑褲,小仙女別忘記了,帶一件保暖的外套,這樣子就可以為自己增添幾分暖意。

第七套:

牛仔真的是時尚圈最潮流的單品之一,不接受反駁,而黑色的牛仔套裝,比起藍色系來說,更加可以凸顯你的個性。再配合一條腰帶,更加的有新意,隨性慵懶兩不誤。

第八套:

氣場十足的風衣,一直都是大家最愛的單品,秋季不穿風衣,真的太對不住自己了。而搭配白色褲子,舒適又休閑,出街肯定不會錯。

第九套:

人人都愛的風衣,卻忽視了墨鏡和包包提升時髦的神奇效應。深色系的穿搭,愛美的女生都穿起來了,美上一整個秋季。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※電子煙有爭議?真相解密

※桃園外送茶莊、喝茶吃宵夜首選

※老司機曝包養內幕

※那些年被包養的回憶實錄

裙子太飄逸就讓白褲子給你優雅氣質

經歷了這麼多年的秋冬,相信大家都知道秋冬中最常見的氣候,就是風大了。

那麼秋風陣陣,裙子太“飄逸”,那就讓白褲子給你優雅氣質,輕鬆提亮造型,讓你每天的心情都是充滿亮光~

卡其色風衣是秋季外套中常青藤,始終屹立不倒。而卡其色風衣本身就自帶高級的氣質感,內搭白色襯衫,氣質瞬間就翻倍了。那麼秋風陣陣,加上白褲子的助陣,整體美感更是氣質得沒話說。褲子的褲腳是採用不規則的剪裁設計,配上黑色短靴,個性又很顯酷。

想將白褲子穿出個性,除了選擇褲腳不規則的款式,還可以將褲腳翻起來穿搭,這樣就能秀出你的個性美了。秋季漸漸颳起“疊穿”的混搭風,白色的小高領,疊穿V領的針織衫,要是外出還覺得冷的話,還可以再來一件針織開衫,輕鬆給你雙倍的溫柔氣質。

在一身白色的造型中,其他配色就變得尤為搶鏡。裙子太“飄逸”,白色針織衫配白色褲子,還有腳上的小白鞋,三個單品連成一線。上衣的內搭是黑色,這個會顯得有些低調。但是腳上的紅色襪子,成為整個造型中最搶鏡的,想不時髦都難。

西裝是秋季外出必須會帶的一件外套,那麼小仙女們都會選擇什麼樣的款式呢?要是我的話,我就會選這身格紋西裝外套,經典又復古,輕鬆順應時尚的潮流。秋風陣陣,配上白色的九分褲,穿出年輕的青春氣息。一雙黑色帆布鞋的助陣,更是完美演繹學院風。

清爽的藍白配延續至今,相信今後會越來越火~裙子太“飄逸”,而在這身look中,白色的直筒褲,扮演着很重要的角色,特別是對於自己的腿型不自信的小仙女,這樣一穿,就能展現你的迷人氣質了。腳上的鞋子與身上的藍色襯衫相呼應,給人一種舒適的靜謐感。

同色系穿搭具有延長身材高度的效果,尤其是下裝與鞋子的同色系look。看這身白色褲子給你優雅氣質,配上小白鞋,不僅讓穿搭顯得更亮,瞬間也讓氣質變成了雙倍。

將紅色外套的扣子都扣起來,這樣看起來很有“剛正不阿”的氣場。而誰說白色不顯瘦的呢?那就讓白色闊腿褲給你優雅氣質,分分鐘“打你臉”,讓紅色的吸睛效果發揮得“物超所值”,讓你美得更自信。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※全台最大電子煙交易平台?

※新竹外送茶交流平台

※老司機曝包養內幕

※那些年被包養的回憶實錄

女人不管多大年齡段,像陳數選擇基礎款搭配

現在這個季節已經比較冷了,所以大家在平常也可以搭配一些大衣來給自己增添更多的時尚感,而且大衣也具有着非常好的保暖效果,我們可以像陳數這樣在裏面搭配上簡單的t恤,這樣就可以讓我們在呈現出時尚度的同時又展露出一定的時尚品味,讓整個人更加精緻一些。另外大家也要注意,千萬不要選擇那些過於花哨的大衣,這樣會讓整個人看上去特別的做作,所以我們還是挑選一些比較低調的衣服比較合適。

女人過40歲穿衣搭配少犯錯,選簡約衣服時尚單品很好搭,時髦大氣,像陳數這樣的衣服搭配就很fashion,雖然都是基礎款的衣服單品,但是搭配起來超級fashion,個性十足。卡其色的外套特別的能給女性塑造出溫柔的感覺,而且像這樣的顏色非常的好搭配衣服,既不會顯得突兀又不會顯得單調,還能夠讓我們呈現出一種非常時尚的感覺,這樣就可以讓大家的個人氣質得到很好的提升,又可以展現出一定的優雅氣息。

此外,再加上一些流蘇的設計,就可以讓這件衣服顯得更加的靈動一些,不會太死板。其實大家在挑選衣服的時候,也可以去選擇一些很具有設計感的衣服。這能夠給我們帶來更多的活力感,而且也不會讓整個人看上去太沉悶,反而能夠增添一些青春的感覺。

女人不管多大年齡段,像“陳數”選擇基礎款搭配,不顯老氣還時髦,搭配衣服就是要會選,選擇到適合你的,才是最好的。我們可以在裏面配上一件比較簡單的T恤,這樣就會帶來更加簡約的感覺,也不會讓整體搭配顯得太誇張,畢竟中年女人還是要穿的簡單一點,這樣才能夠讓她們的優雅魅力得到很好的展現。

白色運動鞋能夠給我們帶來更加休閑的感覺,而且能夠增添一些潮流感,所以會讓這一身搭配更加具有時尚度,這樣也讓陳數看上去非常的青春,具有一定的活力氣息。

大家平常在選擇配飾的時候也可以帶有一點自己的風格,像陳數給自己搭配的就是一串佛珠,她不像現在很多女明星那樣,很喜歡挑選珠寶來作為配飾,陳數挑選的配飾很具有她的個人風格,而且也為她帶來了一些不一樣的感覺,讓她的整體搭配更加具有魅力。

現在有很多女性都特別喜歡短髮的造型,這樣可以讓她們看上去更加有幹練的感覺,而且使用短髮的造型能夠節約我們出門的時間,這樣就可以提高我們的效率,而且還能夠為整個人打造出非常精緻的感覺,特別有氣場。

帶有荷恭弘=恭弘=叶 恭弘 恭弘邊設計的連衣裙具有很不錯的減齡效果,它能夠讓我們看上去更加年輕一些,而且陳數的這一條白色的連衣裙,除了加入了荷恭弘=恭弘=叶 恭弘 恭弘邊的設計之外,還使用了不規則的剪裁,因此能夠打造出一定的層次感,讓這一身搭配更加具有時尚的感覺。而且還能夠讓她整個人看上去更加具有優雅的魅力,所以我們在日常生活中也可以形容這樣的搭配來讓自己呈現出更加精緻的感覺。

大家平常在選擇連衣裙的時候也也一定要注意它的版型,因為不一樣的版型能夠給我們帶來不一樣的效果,有些直筒的版型能夠讓我們的身材得到很好的修飾,而有些修身的設計能夠展露出我們的身材曲線,如果大家的身材像陳數一樣非常完美,那我們就可以去選擇修身的設計,這樣就可以讓我們的身材優勢得到很好的展現。

平常我們在生活中也可以去選擇一些比較鮮艷的連衣裙,或者是比較具有少女感的連衣裙,我們可以用粉紅色來作為配色,這樣就會讓整個人看上去特別的甜美,為女性帶來更多的魅力,而且陳數身上的這條粉色的連衣裙還使用了蛋糕裙的設計,因此會更加的具有夢幻感一些,讓她整個人看上去特別的具有優雅的氣息。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※網友分享與台中外送茶店搞笑趣事

※你應該要知道的電子煙懶人包!

※老司機曝包養內幕

※那些年被包養的回憶實錄