Kubernetes 1.28 スケジューラ OOM の根本原因を探る

Kubernetes 1.28 スケジューラ OOM の根本原因を探る

1. 問題の説明

数年前、同僚が K8s スケジューラを 1.28.3 にアップグレードしたところ、メモリ異常が観察されました。ぜひ見てください。ビジネスの潮流に合わせてクラスター ポッドとノードが変化すると、メモリは OOM まで継続的に上昇傾向を示します。 (以下のデータはすべてコミュニティからのものです)

写真

トリガーとなるシナリオは 2 つあります (コミュニティには再現するための他の方法もあります)。

ケース1

 for (( ; ; )) do kubectl scale deployment nginx-test --replicas=0 sleep 30 kubectl scale deployment nginx-test --replicas=60 sleep 30 done

ケース2

 1. Create a Pod with NodeAffinity under the situation where no Node can accommodate the Pod. 2. Create a new Node.

コミュニティでは類似したメモリ例外シナリオが多数見つかりましたが、再現方法はそれぞれ異なります。上記の問題に関する結論は次のとおりです。

Kubernetes コミュニティはバージョン 1.28 でスケジューリング機能 SchedulerQueueingHints をデフォルトで有効にしましたが、これによりスケジューリング コンポーネントでメモリ例外が発生しました。メモリやその他の問題を一時的に解決するために、コミュニティは 1.28.5 でこの機能をデフォルトで無効にするように調整しました。問題は完全には修正されていないため、この機能を慎重に有効にすることをお勧めします。

2. 技術的背景

この章では以下の内容を紹介します。

  • K8s スケジューラ関連の構造の紹介
  • K8s スケジューラ QueueingHint の紹介
  • Golang の双方向リンクリスト

スケジューラの紹介

PriorityQueue は SchedulingQueue のインターフェース実装です。そのヘッドには、スケジュールされる優先度が最も高いポッドが格納されます。 PriorityQueue には次の重要なフィールドが含まれています。

  1. activeQ: スケジュールする準備ができている Pod を保存します。新しく追加されたポッドはこのキューに配置されます。スケジューリング キューがスケジューリングを実行する必要がある場合、キューから Pod を取得します。 activeQ はヒープによって実装されます。
  2. backoffQ: さまざまな理由 (ノード要件を満たしていないなど) によりスケジュールできないと判断された Pod を保存します。これらのポッドは、バックオフ期間後に activeQ に移動され、再度スケジュールされるようになります。 backoffQ もヒープによって実装されます。
  3. unschedulablePods: さまざまな理由でスケジュールできない Pod を保存します。マップデータ構造です。これらのポッドはスケジュール不可能であると見なされ、backoffQ に直接配置されることはありませんが、ここに記録されます。条件が満たされると、activeQ または backoffQ に移動され、スケジュール キューは unschedulablePods 内の Pod を定期的にクリーンアップします。
  4. inFlightEvents: golang で実装された双方向リンクリストに基づいて、ディスパッチキュー (エントリの値は clusterEvent) と処理中の Pod (エントリの値は *v1.Pod) によって受信されたイベントを保存するために使用されます。
  5. inFlightPods: Pop されたがまだ Done を呼び出していないすべての Pod の UID を保存します。つまり、現在処理中 (スケジュール中、承認中、またはバインディング サイクル中) のすべての Pod です。
 // PriorityQueue implements a scheduling queue. type PriorityQueue struct { ... inFlightPods map[types.UID]*list.Element inFlightEvents *list.List activeQ *heap.Heap podBackoffQ *heap.Heap // unschedulablePods holds pods that have been tried and determined unschedulable. unschedulablePods *UnschedulablePods // schedulingCycle represents sequence number of scheduling cycle and is incremented // when a pod is popped. ... // preEnqueuePluginMap is keyed with profile name, valued with registered preEnqueue plugins. preEnqueuePluginMap map[string][]framework.PreEnqueuePlugin ... // isSchedulingQueueHintEnabled indicates whether the feature gate for the scheduling queue is enabled. isSchedulingQueueHintEnabled bool }

K8s スケジューラの概要については、「Kubernetes スケジューリング (初心者から上級者まで): フレームワーク」を参照してください。最新の K8s スケジューラの概要は後で更新されます。

QueueingHint の紹介

K8s スケジューラでは、QueueingHint 機能が導入されました。この機能は、各プラグインから Pod の再キューイングに関する提案を取得することで、不要なスケジューリングの再試行を減らし、スケジューリングのスループットを向上させます。同時に、Pod のスケジューリング効率をさらに向上させるために、適切な場合にはバックオフがスキップされます。

需要背景

現在、各プラグインは、EventsToRegister を介してプラグインによって拒否された Pod のスケジュールを再試行するタイミングを定義できます。

たとえば、新しく追加または更新されたノードには、Pod の NodeAffinity と一致するラベルがある可能性があるため、NodeAffinity はノードが追加または更新されると Pod のスケジュールを再試行します。ただし、実際にはクラスター内で多数のノード更新イベントが発生するため、NodeAffinity によって以前に拒否された Pod が正常にスケジュールされるとは限りません。

この問題に対処するために、スケジューラは、無関係なイベントを除外するより洗練されたコールバック関数を導入し、正常にスケジュールされる可能性が高い Pod のみが次のスケジュール サイクルで再試行されるようにします。

さらに、DRA (Dynamic Resource Allocation) スケジューリング プラグインは、デバイス ドライバーからのステータス更新を待機するために Pod を拒否する必要がある場合があります。したがって、一部のポッドは、スケジュールされる前に複数のスケジュール サイクルを経る必要がある場合があります。この場合、フォールバックの待機時間は、デバイス ドライバーのステータス更新を待機する時間よりも長くなります。したがって、スケジュールのパフォーマンスを向上させるために、プラグインが特定の状況でフォールバックをスキップできるようにすることが望ましいでしょう。

目標を達成する

スケジューリングのスループットを改善するために、コミュニティは次の改善を提案しました。

  1. QueueingHint の紹介
  • EventsToRegister メカニズムに QueueingHint を導入し、プラグインが Pod の再キューイングに関する提案を提供できるようにしました。
  1. 強化されたポッド追跡および再キューイング メカニズム:
  • スケジューリングキューで処理されるポッドの追跡の実装を最適化します。
  • 拒否されたポッドを適切なキューに再キューイングするメカニズムを実装する
  • 拒否されたポッドのバックオフ戦略を最適化すると、プラグインは特定の状況でバックオフをスキップできるようになり、スケジューリングのスループットが向上します。

潜在的なリスク

1) 実装上のエラーにより、unschedulablePods で Pod が長時間スケジュール不可能になる場合があります。

プラグインが QueueingHint で構成されているが、Pod をスケジュール可能にするいくつかのイベントを見逃した場合、プラグインによって拒否された Pod は、長時間 unschedulablePods に留まる可能性があります。

ただし、スケジューリング キューは、unschedulablePods 内の Pod を定期的にクリーンアップします。 (デフォルトは5分、設定可能)

2) メモリ使用量の増加

スケジューリング キューはスケジューリング プロセス中に発生したイベントを保持する必要があるため、kube-scheduler のメモリ使用量が増加します。したがって、クラスターがビジー状態になればなるほど、より多くのメモリが必要になる可能性があります。

メモリの増加を完全になくすことはできませんが、キャッシュされたイベントをできるだけ早く解放すれば、メモリの増加を遅くすることができます。

3) EnqueueExtensionのEventsToRegisterにおける主な変更

カスタム スケジューラ プラグインの開発者は、互換性をアップグレードする必要があります。 EnqueueExtension の EventsToRegister は、戻り値を ClusterEvent から ClusterEventWithHint に変更します。 ClusterEventWithHint を使用すると、各プラグインは QueueingHintFn というコールバック関数を通じて、より多くの無駄なイベントをフィルター処理できるようになります。

移行を簡素化するために、空の QueueingHintFn は常に Queue を返すものと見なされます。したがって、既存の動作を維持したいだけの場合は、ClusterEvent を ClusterEventWithHint に変更するだけでよく、QueueingHintFn を登録する必要はありません。

QueueingHints デザイン

EventsToRegisterメソッドの戻り値の型が[]ClusterEventWithHintに変更されました。

 // EnqueueExtensions 是一个可选接口,插件可以实现在内部调度队列中移动无法调度的Pod。可以导// 致Pod无法调度(例如,Filter 插件)的插件可以实现此接口。 type EnqueueExtensions interface { Plugin ... EventsToRegister() []ClusterEventWithHint }

各 ClusterEventWithHint 構造には、ClusterEvent と QueueingHintFn が含まれます。イベントが発生すると、QueueingHintFn が実行され、イベントが Pod のスケジュールを満たすことができるかどうかが判断されます。

 type ClusterEventWithHint struct { Event ClusterEvent QueueingHintFn QueueingHintFn } type QueueingHintFn func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (QueueingHint, error) type QueueingHint int const ( // QueueSkip implies that the cluster event has no impact on // scheduling of the pod. QueueSkip QueueingHint = iota // Queue implies that the Pod may be schedulable by the event. Queue )

QueueingHintFn 型は、戻り値の型が (QueueingHint、error) である関数です。このうち、QueueingHintは列挙型であり、取り得る値はQueueSkipとQueueです。 QueueingHintFn は、Pod を unschedulableQ から backoffQ または activeQ に移動する前に呼び出されます。エラーが返された場合、呼び出し元によって返された QueueingHint は QueueAfterBackoff として処理されます。この処理により、返された結果に関係なく、Pod が unschedulableQ キューに永久に留まるのを防ぐことができます。

a.バックオフをスキップする/スキップしないタイミング

BackoffQ は、「長期間スケジュールできない」ポッドがキューをブロックするのを防ぐことで、高いスループットを維持する軽量キューです。

スケジューリング サイクル中に Pod が拒否される回数が増えるほど、Pod が待機する必要がある時間が長くなり、つまり、BackoffQ に留まる時間が長くなります。

たとえば、NodeAffinity が Pod を拒否し、その後 QueueingHintFn でキューを返す場合、Pod はスケジューリングを再試行する前にバックオフを待つ必要があります。

ただし、一部のプラグインは、スケジュール サイクル中に何らかの障害が発生するように設計されています。たとえば、組み込みプラグイン DRA (Dynamic Resource Allocation) は、Reserve 拡張機能で、リソース ドライバーにスケジュール結果を伝え、リソース ドライバーからの応答を待機するために Pod を一度拒否します。この拒否は、スケジュール サイクルの無駄とは見なされません。特定のスケジューリング サイクルが失敗しても、このサイクルに基づくスケジューリング結果によって Pod のスケジューリングが促進される可能性があります。したがって、この理由で拒否されたポッドをバックオフする必要はありません。

この状況をサポートするために、新しい状態「保留中」を導入します。 DRA プラグインが Pending で Pod を拒否し、その後 QueueingHintFn で Queue を返すと、Pod はバックオフをスキップして再スケジュールされます。

b. QueueingHintの仕組み

K8s クラスター イベントが発生すると、スケジューリング キューは、前回のスケジューリング サイクルで Pod を拒否したプラグインの QueueingHintFn を実行します。

次のシナリオでは、Pod の実行方法と移動方法について説明します。

Podは1つ以上のプラグインによって拒否されました

ノードが 3 つあると仮定します。 Pod がスケジューリング サイクルに入ると、1 つのノードはリソース不足のために Pod を拒否し、他の 2 つのノードは Pod の NodeAffinity が一致しなかったために Pod を拒否しました。

この場合、Pod は NodeResourceFit および NodeAffinity プラグインによって拒否され、最終的に unschedulableQ に配置されます。

その後、登録されたプラグインでクラスター イベントが発生するたびに、ディスパッチ キューは QueueingHint を通じてプラグインに通知します。 NodeResourceFit または NodeAffinity のいずれかからの QueueingHintFn がキューを返す場合は、Pod を activeQ または backoffQ に移動します。 (たとえば、NodeAdded イベントが発生すると、Pod が新しいノードにスケジュール可能である可能性があるため、NodeResourceFit の QueueingHint は Queue を返します。)

この Pod が unschedulableQ に留まっていた時間に応じて、activeQ または backoffQ に移動されます。 Pod が unschedulableQ に留まる時間が Pod の予想されるバックオフ遅延よりも長い場合、その Pod は activeQ に直接移動されます。それ以外の場合は、backoffQ に移動します。

保留中のステータスのためポッドは拒否されました

DRA プラグインが予約拡張フェーズで Pod に対して Pending を返すと、スケジューリング キューは DRA プラグインを Pod の pendingPlugins 辞書に追加し、Pod はスケジューリング キューに返されます。

DRA プラグインが QueueingHint への後続の呼び出しでキューを返すと、スケジューリング キューはこの Pod を直接 activeQ に配置します。

 // Reserve reserves claims for the pod. func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleState, pod *v1.Pod, nodeName string) *framework.Status { ... if numDelayedAllocationPending == 1 || numClaimsWithStatusInfo == numDelayedAllocationPending { ... schedulingCtx.Spec.SelectedNode = nodeName logger.V(5).Info("start allocation", "pod", klog.KObj(pod), "node", klog.ObjectRef{Name: nodeName}) ... return statusUnschedulable(logger, "waiting for resource driver to allocate resource", "pod", klog.KObj(pod), "node", klog.ObjectRef{Name: nodeName}) } ... return statusUnschedulable(logger, "waiting for resource driver to provide information", "pod", klog.KObj(pod)) }

紀元前スケジューリングキューで処理されているポッドを追跡する

QueueingHint を導入することで、特定のイベントが発生したときのみスケジュールを再試行できるようになります。しかし、ポッドのスケジュール中にこれらのイベントが発生した場合はどうなるでしょうか?

スケジューラはクラスター データのスナップショットを取得し、そのスナップショットに基づいてポッドをスケジュールします。スナップショットは、スケジュール サイクルが開始されるたびに更新されます。つまり、同じスケジュール サイクルでは同じスナップショットが使用されます。

たとえば、Pod がスケジュールされているが、Pod の NodeAffinity に一致するノードがないため拒否されるが、スケジュール プロセス中に Pod の NodeAffinity に一致する新しいノードが追加されるというシナリオを考えてみましょう。

前述のように、この新しいノードはこのスケジューリング サイクル中に候補ノードとは見なされないため、Pod はノード アフィニティ プラグインによって拒否されます。問題は、スケジューリング キューが Pod を unschedulableQ に配置する場合、Pod のノード アフィニティ要件に一致するノードがすでに存在する場合でも、Pod は別のイベントを待機する必要があることです。

スケジューリング中にポッドがイベントを見逃すシナリオを回避するために、スケジューリング キューはポッドのスケジューリング中に発生するイベントを記録し、これらのイベントと QueueingHint に基づいてポッドがキューに入れられる場所を決定します。

したがって、スケジューリング キューは、ポッドがスケジューリング キューを離れてから、ポッドがスケジューリング キューに戻るかスケジュールされるまで、すべてのイベントをキャッシュします。キャッシュされたイベントが不要になった場合、キャッシュされたイベントは破棄されます。

Golang 二重連結リスト

*list.List は、Go 言語標準ライブラリの container/list パッケージ内のデータ構造であり、二重リンク リストを表します。 Go では、二重リンク リストは、要素の挿入、削除、トラバーサルなどの操作で効率的なパフォーマンスを提供するために使用される一般的なデータ構造です。

以下は *list.List 構造の簡単な紹介です。

  • 定義: *list.List は、二重リンク リストへのポインターであり、リンク リストの先頭と末尾のポインター、およびリンク リストの長さ情報が含まれます。
  • 機能: 二重リンク リストの各ノードには、前のノードと次のノードへのポインターが含まれているため、リンク リスト内の要素の挿入と削除の操作が非常に効率的になります。
  • 使用法: *list.List は、頻繁な挿入および削除操作が必要なシナリオ、特に要素数が固定されていない場合や順序が頻繁に変更される可能性がある場合によく使用されます。

次に例を示します。

 package main import ( "container/list" "fmt" ) func main() { // 创建一个新的双向链表l := list.New() // 在链表尾部添加元素l.PushBack(1) l.PushBack(2) l.PushBack(3) // 遍历链表并打印元素for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } }

PushBack メソッドは、リンク リストの末尾に新しい要素を追加し、新しい要素を表す *list.Element ポインターを返します。このポインターは、削除や変更など、要素に対する後続の操作に使用できます。

*list.Element 構造体には、リンク リスト内の前の要素と次の要素へのポインターと、要素の値を格納するフィールドが含まれています。 *list.Element ポインターを返すことで、さらなる操作が必要になったときに、新しく追加された要素に簡単にアクセスできます。二重リンクリストから要素を削除するには、list.Remove() メソッドを使用できます。このメソッドでは、リンク リスト要素を渡し、リンク リストから要素を削除する必要があります。

 package main import ( "container/list" "fmt" ) func main() { // 创建一个新的双向链表myList := list.New() // 在链表尾部添加元素myList.PushBack(1) myList.PushBack(2) myList.PushBack(3) // 找到要删除的元素elementToRemove := myList.Front().Next() // 从链表中移除该元素myList.Remove(elementToRemove) // 打印剩余的元素for element := myList.Front(); element != nil; element = element.Next() { fmt.Println(element.Value) } }

このコードは以下を出力します。

 1 3

この例では、リンク リストの 2 番目の要素 (値は 2) を削除します。

3. 簡単な分析

メモリ使用量を分析するには、直接 pprof にアクセスします。 pprof リストの一部は次のとおりです。

写真

ここで、メモリが主に protobuf の Decode に集中していることがわかります。 pprof を具体的に分析しなくても、次の 3 つのアイデアが考えられます。

  • grpc-go にはメモリの問題があるか
  • 囲碁自体に問題があるのでしょうか?
  • K8s メモリの問題

最初の仮定については、grpc-go の関連する問題を調べて、関連するメモリ異常に関する最近の報告がないことがわかります。これは go 自体の問題ではないようですが、THP 関連の問題が見つかりました。これについては後ほど簡単に紹介します。そうなると残る結果は一つ、つまりK8s自体に問題があるということだが、(*FieldsV1).Unmarshalは5年間使われていないので、問題はない可能性が高い。それではpprofを簡単に分析してみましょう。

 k8s.io/apimachinery/pkg/apis/meta/v1.(*FieldsV1).Unmarshal vendor/k8s.io/apimachinery/pkg/apis/meta/v1/generated.pb.go Total: 309611 309611 (flat, cum) 2.62% 6502 . . if postIndex > l { 6503 . . return io.ErrUnexpectedEOF 6504 . . } 6505 309611 309611 m.Raw = append(m.Raw[:0], dAtA[iNdEx:postIndex]...) 6506 . . if m.Raw == nil { 6507 . . m.Raw = []byte{} 6508 . . }

しばらくして:

 k8s.io/apimachinery/pkg/apis/meta/v1.(*FieldsV1).Unmarshal vendor/k8s.io/apimachinery/pkg/apis/meta/v1/generated.pb.go Total: 2069705 2069705 (flat, cum) 2.49% 6502 . . if postIndex > l { 6503 . . return io.ErrUnexpectedEOF 6504 . . } 6505 2069705 2069705 m.Raw = append(m.Raw[:0], dAtA[iNdEx:postIndex]...) 6506 . . if m.Raw == nil { 6507 . . m.Raw = []byte{} 6508 . . }

継続的に増え続ける Pod のリストの中に、未公開のデータがいくつか見つかりました。これは、pprof を使用した以前の分析の結果と一致しているようです。継続的な変更の対象はポッドのみであることがわかりました。そこで、コミュニティがすでにこの問題を解決しているかどうかを確認するために、別のトラブルシューティング方法を試しました。トラブルシューティングのために、minikube を使用して Kubernetes 1.18.5 をローカルで起動しました。幸いなことに、この動作を再現できなかったため、バージョン 1.18.5 以降で問題が修正された可能性があります。

調査範囲をさらに絞り込むために、同僚にこれら 3 つのマイナー バージョン間のコミット レコードを確認するように依頼しました。ついに SchedulerQueueingHints 機能をオフにする PR を見つけました。技術的な背景で述べたように、SchedulerQueueingHints 属性はメモリ増加の問題を引き起こす可能性があります。

PriorityQueue 構造を見ると、isSchedulingQueueHintEnabled を通じて機能の論理処理を制御していることがわかります。 QueueingHint機能が有効になっている場合、PodをスケジュールするためにPopメソッドを実行するときに、inFlightPodsの対応するPodのUIDの同じinFlightEventsのリンクリストを入力する必要があります。

 func (p *PriorityQueue) Pop(logger klog.Logger) (*framework.QueuedPodInfo, error) { p.lock.Lock() defer p.lock.Unlock() obj, err := p.activeQ.Pop() ... // In flight, no concurrent events yet. if p.isSchedulingQueueHintEnabled { p.inFlightPods[pInfo.Pod.UID] = p.inFlightEvents.PushBack(pInfo.Pod) } ... return pInfo, nil }

では、リンク リスト フィールドはいつ削除されるのでしょうか?削除されるのは、ポッドがスケジュール サイクルを完了したとき、つまり Done メソッドが呼び出されたときだけであることがわかります。

 func (p *PriorityQueue) Done(pod types.UID) { p.lock.Lock() defer p.lock.Unlock() p.done(pod) } func (p *PriorityQueue) done(pod types.UID) { if !p.isSchedulingQueueHintEnabled { // do nothing if schedulingQueueHint is disabled. // In that case, we don't have inFlightPods and inFlightEvents. return } inFlightPod, ok := p.inFlightPods[pod] if !ok { // This Pod is already done()ed. return } delete(p.inFlightPods, pod) // Remove the pod from the list. p.inFlightEvents.Remove(inFlightPod) for { ... p.inFlightEvents.Remove(e) } }

ここで、完了時間が遅くなるほど、メモリの増加がより顕著になることがわかります。 Pod イベントが無視されたり、見逃されたりすると、リンク リストのメモリも異常に増加します。上記のシナリオに対するいくつかの修正方法を確認できます。

  • できるだけ早くDone()を呼び出すというPRがあります。PR#120586を参照してください。
  • NodeAffinity/NodeUnschedulable プラグインの QueueingHint は関連するノード イベントを見逃します。PR#122284 を参照してください。

著者の時間、視野、知識が限られているため、この記事には誤りや欠落が含まれる可能性があります。読者や業界の専門家が訂正したり意見を交換したりしてくれることを期待しています。

参考文献

1. https://github.com/kubernetes/kubernetes/issues/122725

2. https://github.com/kubernetes/kubernetes/issues/122284

3. https://github.com/kubernetes/kubernetes/pull/122289

4. https://github.com/kubernetes/kubernetes/issues/118893

4. https://github.com/kubernetes/enhancements/blob/master/keps/sig-scheduling/4247-queueinghint/README.md?plain=1#L579

5. https://github.com/kubernetes/kubernetes/issues/122661

6. https://github.com/kubernetes/kubernetes/pull/120586

7. https://github.com/kubernetes/kubernetes/issues/118059

この記事はWeChatの公開アカウント「DCOS」から転載したもので、著者は「DCOS」で、以下のQRコードを通じてフォローできます。

この記事を転載する場合は、「DCOS」公開アカウントまでご連絡ください。

<<:  Kubernetes クラスターで Iptables を Ipvs に置き換える方法

>>:  CKA 試験に合格する可能性を高めます。この記事では、RBAC 権限制御について包括的に理解できます。

推薦する

病院ウェブサイトの SEO の発展はどこに向かうのでしょうか?

現在、百度は多くの変化を遂げており、医療業界の病気用語のランキングを見ると、最適化されたサイトの痕跡...

fatcow 中秋節 1 USD 主催者登録イベント

中国の国力が増すにつれ、外国人は国内のIDC市場にますます注目するようになっています。fatcowの...

SEO にはコツはありません。努力によって達成されます。

最近新しいウェブサイトを始めたのですが、過去の最も苦しい日々に戻ってしまったようです。毎日編集したり...

spinservers: 109 ドル、サンノゼ データ センター、10Gbps 帯域幅、2*e5-2650L v3/64g メモリ/1.6T SSD

spinservers (MET のブランド、1994~) は現在、サンノゼ データ センターの専用...

最小限の費用で最も手頃な入札プロモーションを行う方法

過去2日間、何人かの友人がQQで私に苦情を言いました。彼らの会社のウェブサイトは他のウェブマスターに...

AWS が新しい IoT サービスを発表。機械学習をエッジに導入

[51CTO.com からのオリジナル記事] 本日の AWS re:Invent カンファレンスで、...

お知らせ:海底光ケーブルの故障により海外へのアクセスが遅くなる

最近、海外のウェブサイトへのアクセス速度が非常に遅いことに気づいた人は多いでしょう。海外の VPS ...

急速に広がる: WeChat Kol 精密マーケティングを通じて企業がブランドを強化する方法

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています新しいメデ...

ウェブサイトナビゲーション開発の分析: どのようなナビゲーションウェブサイトが必要ですか?

1. ナビゲーションウェブサイトとは何ですか?ナビゲーションウェブサイトはURLナビゲーションとも呼...

マーケティングプロモーションの2つの本質:コンテンツとチャネル!

私は長年マーケティングプロモーションに携わってきました。毎年、商品プロモーションに対する理解は前年よ...

自分の人生を大切にし、「匿名」アプリケーションには近づかないでください

最近、「Secret」と呼ばれる匿名アプリケーションが友人の間で非常に人気があります。私がこのアプリ...

5G時代のクラウドゲームの発展

クラウド ゲームでは、ローカル ハードウェア操作の代わりにネットワークを使用して画像を送信します。一...

今後10年間の中国の電子商取引の発展パターンが明らかになった:PCではアリババ、モバイルではテンセントとJD

【IT Times Weekly編集部注】中国の電子商取引の漠然とした発展パターンは、JD.comと...

モバイル時代の検索危機: 大量のリンクからユーザーを救えるのは誰か?

収益の96%を広告から得ているインターネット大手のGoogleは、モバイル時代において良い成績表を出...