電流制限の高い同時実行、最後には限界何地獄(細長い記事)

あなたは非常に並行システムはこの事を制限する必要が知っているかもしれないが、何であるかの具体的な制限は、何をすべきか、または2をコピーすることができます。私たちは、その後、体系的にそれに少しクラスを行く、私はあなたの助けを願っています。

Googleのグアバは、電流制限を実現提供:RateLimiterを、この種の設計は非常にコンパクトで、ほとんどの日常業務に適用することができ流控たシーンが、使用シナリオの多様性を与え、また、非常に注意する必要があります。

すでに2件の簡単な記事を使用するように予熱されています。
セマフォの制限、並行性の高いシナリオは秘密があることを言わなければならない
、高い同時通話が高い同時実行と呼ばれていない、暖かいではありません

この時間は異なります。この記事では、深さで、様々なシーンや制限性に導入詳細こと、及びその後コアソースグアバにこの電流制限を分析し、その特性を要約します。少し高度属する高度な記事。

シーンを制限

あなたは、リソースの制約を把握する必要があり、このプロセスは、最も重要な部分です。私は一般的に3つのカテゴリーに分けます。

エージェントレイヤー

例えばSLB、nginxのゲートウェイまたはその他のサービス層は、典型的に基づいて、限定的なサポート连接数、(又は同時)请求数電流を制限します。限定的な寸法は、一般にIPアドレス、リソースの場所、ユーザのサインなどに基づきます。さらに、それはまた、動的に負荷に応じて(ベンチマーク)を制限する政策を調整することができます。

発信者サービス

サービスの発信者が、またサービスの遠位端に速度制限を呼び出すことができるクライアントは、閾値を超え、ローカル限界と呼ばれるブロックまたは直接拒否することができ、電流制限があります协作方

サービス受信者

フローシステムの搬送能力を超えたときに上記と同様、それはサービスの拒否を指示します。通常、信頼性に関する考慮事項アプリケーション自体に基づいて、電流制限に属します主体方私たちは、多くの場合、電流制限は、一般的に、ここで発生することを言います。この論文では、主正方形の方法電流制限に基づいてRateLimiter議論と組み合わせ、その他は同様です。

制限ポリシー

時々、非常に単純な、時には非常に複雑であるが、3に共通のポリシーを制限します。他のすべてが、これらの改善と拡張に行われます。

よる制限同時実行レベル

これは、実施の形態を制限する実装が容易、シンプルで、信号の量は、我々が先に言及したのjavaを達成するために使用することができます。また、比較的利用シナリオ独特の機能を備えています。

1)各要求、必要なリソースの支出が、例えば、各要求は、CPU、IO消費量を消費する、よりバランスしている類似して、要求のRT時間は基本的に同様です。
2)要求または疎密度又は高周波、我々は、この懸念はありません。
3)リソース自体が面倒初期化(必要としない预热)、または支出の初期化は無視することができます。(複雑さを追加します)
4)オーバーフロー・トラフィックの戦略は、比較的簡単で、通常は治療直接拒绝が多い待って失敗したことを意味するので、代わりに待っています。

この戦略は、プロキシ層、ミドルウェア、および同時接続数の他の制限としてフローの最上位レベルのコンポーネントに適用可能です。資格を取得しようとしたときにタイムアウトと呼ばれます溢出等待Bは右、グレードに非常にロードされた単語の上にいるのですか?

リーキーバケットアルゴリズム

リクエスト不確かなアプリケーション・リソース、一定速度でプログラム処理で流量、基本的な原理はバケットアルゴリズムを排出することです。アイスクリームの製造工程のようなビット。リーキーバケットモデルについて-.-には、関連する情報を見に行くことができます。

いくつかの一般的な概念があります。

桶バッファ

まずオーバーフローキューが、要求が拒否された場合、要求をキューに入れてみてください。キューに入った後、リクエストが実行を待っています。

要求が実際に実行されたときにこのように、保留中の要求キューの数に直接的に依存する変数の数が、あります。時にはデザイナーの好き嫌いが待機するように閾値限界要求時間を増やすことを検討します、この時間は、チームへの要求、チームの最大の違いです。バッファサイズのデザインは、通常、直接率に関連します。

漏れ:チームからのリクエスト

このチームのうち、達成するためにいくつかのストレス、異なる設計思想はやや異なっています。そこ抢占式、そこ调度式前記にかかわらず、現在のスレッド(またはプロセス)の、新たなポーリング要求キューバッファの後に処理されるレート(例えばnginxのワーカープロセスとして、またはプロセス)処理スレッドが「先制する」処理速度要求のセットを超えこの戦略のバッファサイズは、上側レートを定義することです。

スケジューリング式は、追加のディスパッチ・スレッド(プロセス)の必要性を理解することはより容易であり、そしてバッファからの取得要求に設定された速度、および回転方法に厳密に従って、他のワーカースレッドへの要求は、既存のワーカースレッドがビジーである場合、直接新しいスレッドを作成し、目的はレートが保証されていることを確認することです、このように、バッファサイズは、待機時間に依存します。

オーバーフロー

なぜなら、それが応答する(バースト性)トラフィックバーストがほとんど容量に直面しないよう制限するリーキーバケット率は、比較的安定しているのは、単に、バッファを超えて、直接拒否。

哀れ何それらを要求します。

トラフィックバースト

バッファは、トラフィックが一定のレベルで破裂、それでもあまりにも壊れやすい、などの瞬間、(最も恥ずかしいが、ほんの少し大きいということである)、高密度を要求し、バッファオーバーフロー、バッファリングして、おそらく「大きな考慮して設計されていますが少しは「合理的な期間内に処理することができ、要求当事者は、いくつかの混乱になるために、」私は、あなたがそう身廊、私に動作しない一連の情報を与えた!!!」ほんの少し余りました。

この戦略はまた、非常に一般的ですが、通常は制限コラボレーティブな方法ではなく、クライアントレベルで適用します。リクエストを発行する前に、オーバーフローがある場合、私たちは上のように、リトライなど信頼性の高い結果を保証するために他の戦略を使用する必要があり、制御フローんが、とにかく、「反対のサービスが圧倒、私を責めないでください、私は自己規律よ。」

トークンバケット

設計モデルは、私が紹介しません、我々は中に深くウィキを見て行くことができます。

トークンバケットの基本的な考え方は、集団的コミューンの時代の古い世代のように、毎月の供給とマーケティングが限度で、それは来月、個々に割り当てられたリソース、およびギャップを持って再び、あなたは信用を並べることができます。

トークンの数は、リソースへのアクセスを許可することができる要求の数は、(我々は、要求ごとに一つだけのトークンと仮定)を。実際に、私たちは本当にそれが必要ではないので、我々はできる利用可能なトークンの数を表すための機能を備えた時間カウンタを使用し、トークンの実体を作成しないでください。リーキー・バケットに比べて、トークンバケット「バケット」は、要求をバッファリングするために使用されていないが、利用可能なリソース(トークン)の量を測定するために使用されます。私たちはトークンエンティティを作成していないが、アプリケーションがトークンを要求しない場合は、まだ仮説的、バケットは毎回のXは、トークンの特定の番号を追加しますすることができますが、その後、トークンバケットがオーバーフローします。..あなたはIO方向からのリーキーバケットアルゴリズムを使用してこのデザインが逆転するでしょう。

次いで欠点リーキーバケットアルゴリズム、また、トークンバケットの専門であることを起こる:トラフィックバースト、その後流速場合、トークンバケットはバッファになったリクエストが低密度であるか、または冷却状態であれば、トークンバケットがオーバーフローします突然、過去に蓄積されたリソースのバランスが、それが直接「借りた。」ことができます

トークンバケットのシナリオの多くを使用するアルゴリズム、適応度の高いが、現実には、トラフィックバーストが一般的ですが、また、設計の観点から、トークンバケット実装が簡単。トピックに戻る、RateLimiterは、考えに基づいてトークンバケットを達成することです。

小さな穴は、より多くの我々は縮小し、最終的に話題に。

RateLimiterの使用

グアバのAPIがより明確に定め、そのデザインのアイデアを入れているが、それはまだこのコメントに少し「哲学の学校」を読んでいる、我々は2個の栗を見て、その後、ソースコードレベルの設計原理から、それを見てください。

//RateLimiter limiter = RateLimiter.create(10,2, TimeUnit.SECONDS);//QPS 100  
RateLimiter limiter = RateLimiter.create(10);  
long start = System.currentTimeMillis();  
for (int i= 0; i < 30; i++) {  
    double time = limiter.acquire();  
    long after = System.currentTimeMillis() - start;  
    if (time > 0D) {  
        System.out.println(i + ",limited,等待:" + time + ",已开始" + after + "毫秒");  
    } else {  
        System.out.println(i + ",enough" + ",已开始" + after + "毫秒");  
    }  
    //模拟冷却时间,下一次loop可以认为是bursty开始  
    if (i == 9) {  
        Thread.sleep(2000);  
    }  
}  
System.out.println("total time:" + (System.currentTimeMillis() - start));   
复制代码

これは、一つだけの資源のフロー制御の簡単な例で、QPSは10、実際のビジネス・シナリオでは、レートが異なるリソースかもしれ我々はそれぞれのサービスのリソースでより多くのlimeter Nを作成することができ、異なるされています。

()メソッドを取得することは許可十分戻って直接待たずに、不十分な場合は、1 / QPS秒待っている場合、トークン(使用を許可、ライセンスのソースコード)を得ることです。

また、あなたは、見つけるリミッターと同様のロック機構()メソッド放出しなかった意味し、「限りアプリケーションとしては、常にそこに、成功します」と終了時に返却する必要はありません。

二つの内部RateLimiterが実装されてあります:(以下、「リソース」、「トークン」、「許可」と同じ意味)

SmoothBursty

サポート「バースト性トラフィック」流れ制限、流量制限時間を使用しない場合、すなわち、さらにバーストトラフィックが速く発生した場合、バースト性トラフィックのために準備するために、いくつかの許可を格納することができ、より完全にリソースを使用し、スムーズな流れ制限された状態で(または許可を使用した後に蓄積期間を、オフ冷却)速度。

焦点は、冷却の間、許可が蓄積し、トラフィックにバーストされ、あなたが許可を任意の待ち時間を必要とせずに蓄積していた消費することができます。人と同じように、より高速を持つことができ、再び起動、実行後にいくつかの時間を離陸。

このように、あなたのリソース場合は、もう一度、通常よりも高い効率を提供するために使用されているいくつかの時間のために(使用されていない)、この時間を冷却した後、あなたはSmoothBurstyを使用することができます。

道の作成

RateLimiter.create(double permitsPerSecond)
复制代码

同様の結果

0,enough,已开始1毫秒  
1,limited,等待:0.098623,已开始105毫秒  
2,limited,等待:0.093421,已开始202毫秒  
3,limited,等待:0.098287,已开始304毫秒  
4,limited,等待:0.096025,已开始401毫秒  
5,limited,等待:0.098969,已开始505毫秒  
6,limited,等待:0.094892,已开始605毫秒  
7,limited,等待:0.094945,已开始701毫秒  
8,limited,等待:0.099145,已开始801毫秒  
9,limited,等待:0.09886,已开始905毫秒  
10,enough,已开始2908毫秒  
11,enough,已开始2908毫秒  
12,enough,已开始2908毫秒  
13,enough,已开始2908毫秒  
14,enough,已开始2908毫秒  
15,enough,已开始2908毫秒  
16,enough,已开始2908毫秒  
17,enough,已开始2908毫秒  
18,enough,已开始2908毫秒  
19,enough,已开始2908毫秒  
20,enough,已开始2909毫秒  
21,limited,等待:0.099283,已开始3011毫秒  
22,limited,等待:0.096308,已开始3108毫秒  
23,limited,等待:0.099389,已开始3211毫秒  
24,limited,等待:0.096674,已开始3313毫秒  
25,limited,等待:0.094783,已开始3411毫秒  
26,limited,等待:0.097161,已开始3508毫秒  
27,limited,等待:0.099877,已开始3610毫秒  
28,limited,等待:0.097551,已开始3713毫秒  
29,limited,等待:0.094606,已开始3809毫秒  
total time:3809  
复制代码

SmoothWarmingUp

ウォーミングアップ(暖機)特性を有する、すなわち、バーストトラフィックが発生し、最大速度はすぐに達成するが、徐々に設定閾値「ウォームアップ時間」内に必要な最終達成するために増加させることができない、その設計思想、及びSmoothBursty反し、突起髪の流れが(最大速度まで)リソースの制御が遅い、漸進的な使用で発生した場合に、一定の流量が制限された状態になった後。

焦点は、資源が使用されているが、それは安定した速度を制限し続けることができ、冷却時間(有効長ウォームアップ間隔)それ以外の場合は、長い長い待ち時間、注意を払う必要があり、冷却時間が許せを蓄積する際に許可を得ることが、これらの許可はまだ待つ必要があり得ます。

それはいくつかの準備作業が必要なときにリソースしばらく(使用しない)冷却した後、再度使用する場合したがって、それは通常より低い効率を提供することができ、そのような接続プーリング、データベースキャッシュとして。

道の作成

RateLimiter.create(double permitsPerSecond,long warnupPeriod,TimeUnit unit)
复制代码

以下の結果は、我々は大きな成長のプロセスがある見ることができます。

0,enough,已开始1毫秒  
1,limited,等待:0.288847,已开始295毫秒  
2,limited,等待:0.263403,已开始562毫秒  
3,limited,等待:0.247548,已开始813毫秒  
4,limited,等待:0.226932,已开始1041毫秒  
5,limited,等待:0.208087,已开始1250毫秒  
6,limited,等待:0.189501,已开始1444毫秒  
7,limited,等待:0.165301,已开始1614毫秒  
8,limited,等待:0.145779,已开始1761毫秒  
9,limited,等待:0.128851,已开始1891毫秒  
10,enough,已开始3895毫秒  
11,limited,等待:0.289809,已开始4190毫秒  
12,limited,等待:0.264528,已开始4458毫秒  
13,limited,等待:0.247363,已开始4710毫秒  
14,limited,等待:0.225157,已开始4939毫秒  
15,limited,等待:0.206337,已开始5146毫秒  
16,limited,等待:0.189213,已开始5337毫秒  
17,limited,等待:0.167642,已开始5510毫秒  
18,limited,等待:0.145383,已开始5660毫秒  
19,limited,等待:0.125097,已开始5786毫秒  
20,limited,等待:0.109232,已开始5898毫秒  
21,limited,等待:0.096613,已开始5999毫秒  
22,limited,等待:0.096321,已开始6098毫秒  
23,limited,等待:0.097558,已开始6200毫秒  
24,limited,等待:0.095132,已开始6299毫秒  
25,limited,等待:0.095495,已开始6399毫秒  
26,limited,等待:0.096352,已开始6496毫秒  
27,limited,等待:0.098641,已开始6597毫秒  
28,limited,等待:0.097883,已开始6697毫秒  
29,limited,等待:0.09839,已开始6798毫秒  
total time:6798  
复制代码

ソースコード解析方法を取得します

上記2つのクラスがSmoothRateLimiterから継承し、最終的RateLimiterから派生; RateLimiter内側コア方法。

1)二重取得():許可適切直接返した場合、許可を取得し、又は1 / QPS秒を待ちます。リターンが待っている、0.0を限定的ではないことを意味場合、このメソッドは、時間(秒)を待機しているスレッドを返します。

2)二重取得(INTのN)、n個の許可を取得し、直接返した場合十分可能にし、そうでなければ制限と待つ、待機時間「許可/ QPSの不足数」です。(そう説明されている時間)

次の擬似コードは、この方法です。

//伪代码  
public double acquire(int requiredPermits) {  
    long waitTime = 0L;  
    synchronized(mutex) {  
          boolean cold = nextTicketTime > now;  
          if (cold) {  
             storedPermits = 根据冷却时长计算累积的permits;  
             nextTicketTime = now;  
          }  
          //根据storedPermits、requiredPermits计算需要等待的时间  
          //bursty:如果storePermits足够,则waitTime = 0  
          //warmup:平滑预热,storePermits越多(即冷却时间越长),等待时间越长  
          if(storedPermits不足) {  
              waitTime += 欠缺的permits个数 / QPS;  
          }  
          if(bursty限流) {  
              waitTime += 0;//即无需额外等待  
          }  
          if(warmup限流) {  
              waitTime += requiredPermits / QPS;  
              if(storedPermits > 0.5 * maxPermits) {  
                waitTime += 阻尼时间;  
              }   
          }  
   
          nextTicketTime += waitTime  
    }  
    if (waitTime > 0L) {  
      Thread.sleep(waitTime);  
    }  
    return waitTime;  
}  
复制代码

以下は、非常に退屈より退屈だろう~~~~~~

図1に示すように、オブジェクトミューテックス:同期ロックは、上記擬似コードのように、在庫許可の計算は、全て同期している(計算含む)実際の適用中に許可し、我々は、同期機構を使用しない内部ロックRateLimiterを知る必要があります。

2、maxPermits:記憶することができるライセンスの最大数(チケットの数)、SmoothBurstyとSmoothWarimingUpデフォルト実装は異なる:
1)SmoothBusty、その値maxBurstSecond * QPSは、「バースト性トラフィック期間」* QPS可能にすることである、これデザインの種類が認識されるであろう、しかし、maxBustSecond RateLimiterこの値は、最終的なQPSに等しい、1.0をハードコードされます。
2)SmoothWarmingUp:デフォルトのアルゴリズムでは、それは単に* QPS「長いウォームアップ時間」で、warmupPeriod * QPSです。

このパラメータの主な制限、どんなに長くこの値storedPermitsを超えることができない冷却; QPSこの値を設定した後、次いで、もはや変化。

3、storedPermits:許可の数が既に格納されているが、この値は冷却時間に依存し、maxPermitsより長い単に冷却時間、大きいこの値が、これ以上は、開始値は0です。
1)可能にするために、要求は、アプリケーションの前に、アプリケーションタイムトークン(nexFreeTicketTime)との間の時間差が計算され、これから、冷却速度(1 / QPS)の間に生成されたトークンに応じて格納されたトークンを計算することができる場合storedPermitsある番号、。
2)アプリケーション許可が完了した後、現在の時刻(必要に応じて、待機中の次の要求トークンの開始時間のような)追加の遅延を追加し、冷却時間が終了します。
アプリケーションが終了した後3)、storedPermitsマイナス許可の数がゼロになるまで適用されます。

冷却時間が増加storePermitsにつながり、前記還元storePermitsもたらす操作を取得し、冷却したときに適用周波数と長さがstoredPermitsサイズを決定します。

4、nextFreeTicketMicros(タイムスタンプ、微妙単位):トークンが自由に利用できる次の時間が、この値は将来の時間であってもよいし、流れ制限を示す。この時間が開始され、要求の一部は、主値と、待たなければなりませんラベルされた「クールな状態。」(クレジット)
1)クールダウン期間ならば、その値はすなわち、この値が現在よりも少ない、通常は過去形です。
2)場合は、この時点で許可を適用するための要求があり、一方、この値は現在の時間差によってこの値は現在、storedPermitsを計算します。
3)この値はstoredPermitsを計算することなく、この値をリセットする必要がない、将来の時間、すなわち、今よりも大きい場合。
4)アプリケーションのチケット後、storedPermitsマイナスようなウォームアップ期間として制限速度トリガ待ちが(不十分許可する場合、必要なチケットの数)から、それはnextFreeTicketsTime値として演算後2)追加の待ち時間であろう。
5)冷却の最初の要求を待機する必要はないが、この値は現在+ WAITTIMEは、次の期間またWAITTIMEでいることを意味する時間を()、待機特性を減衰するように設定された後warmingUpを制限するための2)に基づいて要求は、それが待機をトリガし、nextFreeTicketMicros値に追従していきます。この値の継続、ウォーミングアップ、ダンピングWAITTIME複雑な計算、1 / QPS +付加価値の間に、ウォームアップ時間の成長と付加価値が低減されます。
storedPermitsが0より大きい場合は6)2)に基づいて、バースト性の制限のために、常に今、単に設定されたこの値を待つ必要がなく、それ以外の場合、通常1 / QPS間隔に応じて遅延時間を計算することであるべきポイント。

図5に示すように、分割線は、絞りの通常の待ち時間(アプリケーションの許可の数/ QPS)を使用して、このstoredPermitsよりも小さい場合、閾値として* 0.5パーティングラインmaxPermitsを制限するためのウォームアップ、分割線上記本で、次いで4)付加減衰、減衰予熱。

図6は、我々は内部RateLimiterが本当にチケットエンティティを生成しないことがわかったが、アプリケーションのリソースが(storedPermitsに該当する)場合にのみ、長い冷却時間に基づいて、チケットの在庫が計算されます。かかわらず、電流制限の、storedPermitsが好ましく用いられます。

小概要

総括する時間です。

RateLimiterは、それが追加のロックや同期を必要とせずに、同時実行環境で直接使用することができ、スレッドセーフです。

アカウントに内部同期ロックRateLimiterを取る、我々は、実際のビジネスの発展に、通常、そのRateLimiterではなく、公共のではなく、大量のメモリを使用する(URLなど)各リソースです。

流量制限の内部余分なスレッド、およびなし他のエンティティデータ構造は、チケットを格納するために使用されるので、それは利点である、非常に軽量です。

RateLimiter最大の問題は、常に成功した方法を取得され、チケットの内部時刻が戻って移行します。同時高い臨界率が閾値を超えた場合、後続の要求が制限され、待機時間がタイムラインに基づいて蓄積され、同じ運命とセマフォを被った、制御不能の待ち時間につながります。

取得するために行くことができた場合、上記の問題を避けるために、我々は通常、tryAcquired検出で始まり、トークンが不足している場合は、適切なものは拒否しました。だから、RateLimiterに基づいて、そして何の我々は、追加の開発が必要であること、ポリシーを内蔵し否定できない。

私たちは、単にそれ以外の場合は深刻な問題につながる可能性があり、待ち時間を制限する達成するために取得する方法に依存することはできません。私たちは、「時間遅れのポイントは非常に遠くている」と許容範囲を超えた場合、要求は直接拒否されます、「状態を制限する」、「チケットの遅延時間を持っている」かどうか、追加の属性レコードを使用して、一般的にパッケージ化されRateLimiterが必要。簡単に言えば、パッケージの取得方法は、要求は、直接拒否長い場合には、待っている時間を増やすために決定することができます。

大きな問題のRateLimiterがあり、拡大することはほとんど不可能です:サブクラスが保護されています。ああ、反射を除きます。

練習

またはコードの一部にそれがより明確に私たちが仕事を見ることができます:FollowCotroller.java:フローコントローラは、電流制限開始場合、あなただけの最高のため、直接拒否され、この値以上のものを待つことを要求することができます

public class FollowController {  
  
    private final RateLimiter rateLimiter;  
  
    private int maxPermits;  
  
    private Object mutex = new Object();  
  
    //等待获取permits的请求个数,原则上可以通过maxPermits推算  
    private int maxWaitingRequests;  
  
    private AtomicInteger waitingRequests = new AtomicInteger(0);  
  
    public FollowController(int maxPermits,int maxWaitingRequests) {  
        this.maxPermits = maxPermits;  
        this.maxWaitingRequests = maxWaitingRequests;  
        rateLimiter = RateLimiter.create(maxPermits);  
    }  
  
    public FollowController(int permits,long warmUpPeriodAsSecond,int maxWaitingRequests) {  
        this.maxPermits = maxPermits;  
        this.maxWaitingRequests = maxWaitingRequests;  
        rateLimiter = RateLimiter.create(permits,warmUpPeriodAsSecond, TimeUnit.SECONDS);  
    }  
  
    public boolean acquire() {  
        return acquire(1);  
    }  
  
    public boolean acquire(int permits) {  
        boolean success = rateLimiter.tryAcquire(permits);  
        if (success) {  
            rateLimiter.acquire(permits);//可能有出入  
            return true;  
        }  
        if (waitingRequests.get() > maxWaitingRequests) {  
            return false;  
        }  
        waitingRequests.getAndAdd(permits);  
        rateLimiter.acquire(permits);  
  
        waitingRequests.getAndAdd(0 - permits);  
        return true;  
    }  
  
}  
复制代码

上記のコードは、githubので見つけることができます。

https://github.com/sayhiai/example-ratelimit
复制代码

終わり

あなたは、グアバは非常に軽量かつ包括的なリストリクタを提供し、見ることができます。それは達成するために、複数のスレッドを使用する必要はありませんが、それは、スレッドセーフです。信号の量を比較すると、その使用が簡単。使用している場合しかし、シナリオを制限するの多様性を考えると、同じことは非常に注意しなければなりません。


よりエキサイティングな記事。

マイクロサービスではないすべて、ドメイン固有のサブセットのみ

そんなに監視コンポーネントは、あなたのための権利が常にあります

「サブライブラリーサブテーブル」?選択プロセスは、そうでなければ、彼らが制御不能になり、注意が必要

私たちが開発に持っているもの、最後に、ネッティーを使用しますか?

Linuxの制作環境では、最も一般的に「vimの」スキルセットを使用

おすすめ

転載: juejin.im/post/5d1c978d51882555433429e6