2024年2月


0. 前言

[译] kubernetes:kube-scheduler 调度器代码结构概述
介绍了
kube-scheduler
的代码结构。本文围绕代码结构,从源码角度出发,分析
kube-scheduler
的调度逻辑。

1. 启动 kube-scheduler

kube-scheduler
使用
Cobra
框架初始化参数,配置和应用。

// kubernetes/cmd/kube-scheduler/scheduler.go
func main() {
    // 启动 kube-scheduler 入口
	command := app.NewSchedulerCommand()
	...
}

// kubernetes/cmd/kube-scheduler/app/server.go
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
    // 创建 kube-scheduler 选项
	opts := options.NewOptions()

    cmd := &cobra.Command{
		Use: "kube-scheduler",
		...
		RunE: func(cmd *cobra.Command, args []string) error {
			return runCommand(cmd, opts, registryOptions...)
		},
        ...
    }
    ...
}

// 运行 kube-scheduler
func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error {
	...
    // 创建 kube-scheduler 配置 cc
    // 创建 kube-scheduler 实例 sched
	cc, sched, err := Setup(ctx, opts, registryOptions...)
	if err != nil {
		return err
	}
	...
	return Run(ctx, cc, sched)
}

从启动命令来看,这里重点关注的是
Setup
函数。在该函数内,创建
kube-scheduler
配置
cc
和调度器实例
sched

func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) {
    ...
    // 验证选项
	if errs := opts.Validate(); len(errs) > 0 {
		return nil, nil, utilerrors.NewAggregate(errs)
	}

    // 根据选项创建配置 c
	c, err := opts.Config(ctx)
	if err != nil {
		return nil, nil, err
	}

    // 补充配置为完整配置
    cc := c.Complete()

    // 外部注册插件
    outOfTreeRegistry := make(runtime.Registry)
	for _, option := range outOfTreeRegistryOptions {
		if err := option(outOfTreeRegistry); err != nil {
			return nil, nil, err
		}
	}

    ...
	// 创建调度器实例 sched
    sched, err := scheduler.New(ctx,
		cc.Client,
		cc.InformerFactory,
		cc.DynInformerFactory,
		recorderFactory,
		scheduler.WithComponentConfigVersion(cc.ComponentConfig.TypeMeta.APIVersion),
		scheduler.WithKubeConfig(cc.KubeConfig),
		scheduler.WithProfiles(cc.ComponentConfig.Profiles...),
		scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
		scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry),
		scheduler.WithPodMaxBackoffSeconds(cc.ComponentConfig.PodMaxBackoffSeconds),
		scheduler.WithPodInitialBackoffSeconds(cc.ComponentConfig.PodInitialBackoffSeconds),
		scheduler.WithPodMaxInUnschedulablePodsDuration(cc.PodMaxInUnschedulablePodsDuration),
		scheduler.WithExtenders(cc.ComponentConfig.Extenders...),
		scheduler.WithParallelism(cc.ComponentConfig.Parallelism),
		scheduler.WithBuildFrameworkCapturer(func(profile kubeschedulerconfig.KubeSchedulerProfile) {
			// Profiles are processed during Framework instantiation to set default plugins and configurations. Capturing them for logging
			completedProfiles = append(completedProfiles, profile)
		}),
	)

    ...
    return &cc, sched, nil
}

函数
scheduler.New
创建调度器实例
sched
,进入函数内查看实例是如何创建的。

func New(ctx context.Context,
	client clientset.Interface,
	informerFactory informers.SharedInformerFactory,
	dynInformerFactory dynamicinformer.DynamicSharedInformerFactory,
	recorderFactory profile.RecorderFactory,
	opts ...Option) (*Scheduler, error) {
    ...
    // 注册内置插件
    registry := frameworkplugins.NewInTreeRegistry()

    // merge 内置插件和外部注册插件
	if err := registry.Merge(options.frameworkOutOfTreeRegistry); err != nil {
		return nil, err
	}

    // 注册指标
    metrics.Register()

    // 注册外部扩展器
	extenders, err := buildExtenders(logger, options.extenders, options.profiles)
	if err != nil {
		return nil, fmt.Errorf("couldn't build extenders: %w", err)
	}

    // 实例化 podLister 负责监控 pod 变化
    podLister := informerFactory.Core().V1().Pods().Lister()
    // 实例化 nodeLister 负责监控 node 变化
	nodeLister := informerFactory.Core().V1().Nodes().Lister()

    // 创建 snapshot,snapshot 作为缓存存在
	snapshot := internalcache.NewEmptySnapshot()

    ...
    // 创建 profiles,profiles 中存储的是调度器框架
	profiles, err := profile.NewMap(ctx, options.profiles, registry, recorderFactory,
		frameworkruntime.WithComponentConfigVersion(options.componentConfigVersion),
		frameworkruntime.WithClientSet(client),
		frameworkruntime.WithKubeConfig(options.kubeConfig),
		frameworkruntime.WithInformerFactory(informerFactory),
		frameworkruntime.WithSnapshotSharedLister(snapshot),
		frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(options.frameworkCapturer)),
		frameworkruntime.WithParallelism(int(options.parallelism)),
		frameworkruntime.WithExtenders(extenders),
		frameworkruntime.WithMetricsRecorder(metricsRecorder),
	)

    // 创建 preEnqueuePlugin 插件
    preEnqueuePluginMap := make(map[string][]framework.PreEnqueuePlugin)
    ...

    // 创建优先级队列 podQueue
    podQueue := internalqueue.NewSchedulingQueue(
		profiles[options.profiles[0].SchedulerName].QueueSortFunc(),
		informerFactory,
		internalqueue.WithPodInitialBackoffDuration(time.Duration(options.podInitialBackoffSeconds)*time.Second),
		internalqueue.WithPodMaxBackoffDuration(time.Duration(options.podMaxBackoffSeconds)*time.Second),
		internalqueue.WithPodLister(podLister),
		internalqueue.WithPodMaxInUnschedulablePodsDuration(options.podMaxInUnschedulablePodsDuration),
		internalqueue.WithPreEnqueuePluginMap(preEnqueuePluginMap),
		internalqueue.WithQueueingHintMapPerProfile(queueingHintsPerProfile),
		internalqueue.WithPluginMetricsSamplePercent(pluginMetricsSamplePercent),
		internalqueue.WithMetricsRecorder(*metricsRecorder),
	)

    ...
    // 创建调度器缓存
    schedulerCache := internalcache.New(ctx, durationToExpireAssumedPod)
    ...

    // 实例化调度器
    sched := &Scheduler{
		Cache:                    schedulerCache,
		client:                   client,
		nodeInfoSnapshot:         snapshot,
		percentageOfNodesToScore: options.percentageOfNodesToScore,
		Extenders:                extenders,
		StopEverything:           stopEverything,
		SchedulingQueue:          podQueue,
		Profiles:                 profiles,
		logger:                   logger,
	}

    // 将队列的 Pop 方法赋值给 sched.NextPod
	sched.NextPod = podQueue.Pop
	...

    // 添加 Event 回调 handler
	if err = addAllEventHandlers(sched, informerFactory, dynInformerFactory, unionedGVKs(queueingHintsPerProfile)); err != nil {
		return nil, fmt.Errorf("adding event handlers: %w", err)
	}

	return sched, nil
}

scheduler.New
创建了
snapshot
,
eventHandler
,
profiles(framework)

cache
等对象,结合着调度框架将它们关联起来会更清晰。

2. 运行 kube-scheduler

创建完各个对象之后,接下来运行
kube-scheduler
将各个对象关联起来运行。

func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error {
    ...
    // 选举 leader
    waitingForLeader := make(chan struct{})
	isLeader := func() bool {
		select {
		case _, ok := <-waitingForLeader:
			// if channel is closed, we are leading
			return !ok
		default:
			// channel is open, we are waiting for a leader
			return false
		}
	}

    ...
    // 运行 informer
    startInformersAndWaitForSync := func(ctx context.Context) {
		// Start all informers.
		cc.InformerFactory.Start(ctx.Done())
		// DynInformerFactory can be nil in tests.
		if cc.DynInformerFactory != nil {
			cc.DynInformerFactory.Start(ctx.Done())
		}

		// Wait for all caches to sync before scheduling.
		cc.InformerFactory.WaitForCacheSync(ctx.Done())
		// DynInformerFactory can be nil in tests.
		if cc.DynInformerFactory != nil {
			cc.DynInformerFactory.WaitForCacheSync(ctx.Done())
		}

		// Wait for all handlers to sync (all items in the initial list delivered) before scheduling.
		if err := sched.WaitForHandlersSync(ctx); err != nil {
			logger.Error(err, "waiting for handlers to sync")
		}

		logger.V(3).Info("Handlers synced")
	}
	if !cc.ComponentConfig.DelayCacheUntilActive || cc.LeaderElection == nil {
		startInformersAndWaitForSync(ctx)
	}

    // leader 节点运行调度逻辑,暂略
    if cc.LeaderElection != nil {
        ...
    }

    close(waitingForLeader)
	sched.Run(ctx)
	return fmt.Errorf("finished without leader elect")
}

Run
函数内包含三部分处理:

  • 选举 leader 节点。如果是单节点,则跳过选举。
  • 运行 informer,负责监控 pod 和 node 变化。
  • 运行调度器

进入
sched.Run
查看调度器是如何运行的。

func (sched *Scheduler) Run(ctx context.Context) {
	...
    // 从队列中去需要调度的 pod
	sched.SchedulingQueue.Run(logger)

	// 调度 pod
	go wait.UntilWithContext(ctx, sched.scheduleOne, 0)

	<-ctx.Done()
	...
}

sched.Run
主要做了两件事。从优先级队列中取用于调度的 pod,然后通过
sched.scheduleOne
调度该 pod。

首先,看取调度 pod 的过程,如下。

func (p *PriorityQueue) Run(logger klog.Logger) {
	go wait.Until(func() {
		p.flushBackoffQCompleted(logger)
	}, 1.0*time.Second, p.stop)
	go wait.Until(func() {
		p.flushUnschedulablePodsLeftover(logger)
	}, 30*time.Second, p.stop)
}

优先级队列由
ActiveQ

BackoffQ

UnschedulableQ
组成,其逻辑关系如下。


PriorityQueue.Run
中启动两个
goroutine
分别运行
p.flushBackoffQCompleted

p.flushUnschedulablePodsLeftover
方法。
p.flushBackoffQCompleted
将处于
BackOffQ
的 pod 移到
ActiveQ

p.flushUnschedulablePodsLeftover

UnschedulableQ
的 pod 移到
ActiveQ
或者
BackOffQ
。详细取调度 pod 的逻辑可查看
kube-scheduler 调度队列

接着,进入
sched.scheduleOne
查看 pod 是怎么调度的。

func (sched *Scheduler) scheduleOne(ctx context.Context) {
	...
	// 获取需要调度的 pod
	podInfo, err := sched.NextPod(logger)

	...
	// 进入调度循环调度 pod
	scheduleResult, assumedPodInfo, status := sched.schedulingCycle(schedulingCycleCtx, state, fwk, podInfo, start, podsToActivate)
	if !status.IsSuccess() {
		sched.FailureHandler(schedulingCycleCtx, fwk, assumedPodInfo, status, scheduleResult.nominatingInfo, start)
		return
	}

	// 进入绑定循环绑定 pod
	go func() {
		...
		status := sched.bindingCycle(bindingCycleCtx, state, fwk, scheduleResult, assumedPodInfo, start, podsToActivate)
		...
	}()
}

sched.scheduleOne
主要包括三部分:获取需要调度的 pod,进入调度循环调度 pod 和进入绑定循环绑定 pod。其逻辑结构如下。

进一步,查看每一部分的源码。

2.1
sched.NextPod
获取需要调度的 pod

func (p *PriorityQueue) Pop(logger klog.Logger) (*framework.QueuedPodInfo, error) {
	...
	for p.activeQ.Len() == 0 {
		if p.closed {
			logger.V(2).Info("Scheduling queue is closed")
			return nil, nil
		}

		// 如果 activeQ 没有 pod 的话,阻塞等待
		p.cond.Wait()
	}

	// 从 activeQ 中取 pod
	obj, err := p.activeQ.Pop()
	if err != nil {
		return nil, err
	}
	pInfo := obj.(*framework.QueuedPodInfo)
	...

	return pInfo, nil
}

sched.NextPod
的逻辑主要是看
activeQ
队列中有没有 pod,如果有的话,取 pod 调度。如果没有的话,阻塞等待,直到
activeQ
中有 pod。

2.2
sched.schedulingCycle
调度 pod

func (sched *Scheduler) schedulingCycle(
	ctx context.Context,
	state *framework.CycleState,
	fwk framework.Framework,
	podInfo *framework.QueuedPodInfo,
	start time.Time,
	podsToActivate *framework.PodsToActivate,
) (ScheduleResult, *framework.QueuedPodInfo, *framework.Status) {
	...
	// 调度 Pod
	scheduleResult, err := sched.SchedulePod(ctx, fwk, state, pod)
	...

	assumedPodInfo := podInfo.DeepCopy()
	assumedPod := assumedPodInfo.Pod
	err = sched.assume(logger, assumedPod, scheduleResult.SuggestedHost)
	...

	// 运行 Reserve 插件的 Reserve 方法
	if sts := fwk.RunReservePluginsReserve(ctx, state, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() {
		...
	}

	// 运行 Permit 插件
	runPermitStatus := fwk.RunPermitPlugins(ctx, state, assumedPod, scheduleResult.SuggestedHost)
	if !runPermitStatus.IsWait() && !runPermitStatus.IsSuccess() {
		...
	}

	...
	return scheduleResult, assumedPodInfo, nil
}

sched.schedulingCycle
包含几个步骤:
sched.SchedulePod
调度 Pod,将调度的还未绑定的 Pod 作为 assumedPod 添加到缓存,运行
Reserve
插件和
Permit
插件。

首先,看
sched.SchedulePod
是怎么调度 Pod 的。

func (sched *Scheduler) schedulePod(ctx context.Context, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) {
	feasibleNodes, diagnosis, err := sched.findNodesThatFitPod(ctx, fwk, state, pod)
	if err != nil {
		return result, err
	}
	...
}


sched.SchedulePod
中,
sched.findNodesThatFitPod
为 Pod 寻找合适的节点。

// kubernetes/pkg/scheduler/schedule_one.go
func (sched *Scheduler) findNodesThatFitPod(ctx context.Context, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) ([]*framework.NodeInfo, framework.Diagnosis, error) {
	...
	// 从 snapshot 中取所有节点
	allNodes, err := sched.nodeInfoSnapshot.NodeInfos().List()
	if err != nil {
		return nil, diagnosis, err
	}

	preRes, s := fwk.RunPreFilterPlugins(ctx, state, pod)
	if !s.IsSuccess() {
		...
	}

	...
	// 寻找 pod 可调用的节点
	feasibleNodes, err := sched.findNodesThatPassFilters(ctx, fwk, state, pod, &diagnosis, nodes)
	...
}

// kubernetes/pkg/scheduler/schedule_one.go
func (sched *Scheduler) findNodesThatPassFilters(
	ctx context.Context,
	fwk framework.Framework,
	state *framework.CycleState,
	pod *v1.Pod,
	diagnosis *framework.Diagnosis,
	nodes []*framework.NodeInfo) ([]*framework.NodeInfo, error) {
	...
	checkNode := func(i int) {
		...
		status := fwk.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo)
	}
	...
}

// kubernetes/pkg/scheduler/framework/runtime/framework.go
func (f *frameworkImpl) RunFilterPluginsWithNominatedPods(ctx context.Context, state *framework.CycleState, pod *v1.Pod, info *framework.NodeInfo) *framework.Status {
	...
	for i := 0; i < 2; i++ {
		...
		// 运行 Filter 插件
		status = f.RunFilterPlugins(ctx, stateToUse, pod, nodeInfoToUse)
		if !status.IsSuccess() && !status.IsRejected() {
			return status
		}
	}

	return status
}

sched.findNodesThatFitPod
运行
Filter
插件获取可用的节点
feasibleNodes
。接着,如果可用的节点只有一个,则返回调度结果。如果有多个节点则运行 priority 插件寻找最合适的节点作为调度节点。逻辑如下。

func (sched *Scheduler) schedulePod(ctx context.Context, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) {
	...
	feasibleNodes, diagnosis, err := sched.findNodesThatFitPod(ctx, fwk, state, pod)
	if err != nil {
		return result, err
	}

	...
	if len(feasibleNodes) == 1 {
		return ScheduleResult{
			SuggestedHost:  feasibleNodes[0].Node().Name,
			EvaluatedNodes: 1 + len(diagnosis.NodeToStatusMap),
			FeasibleNodes:  1,
		}, nil
	}

	priorityList, err := sched.prioritizeNodes(ctx, fwk, state, pod, feasibleNodes)
	if err != nil {
		return result, err
	}

	host, _, err := selectHost(priorityList, numberOfHighestScoredNodesToReport)
	...

	return ScheduleResult{
		SuggestedHost:  host,
		EvaluatedNodes: len(feasibleNodes) + len(diagnosis.NodeToStatusMap),
		FeasibleNodes:  len(feasibleNodes),
	}, err

获得调度结果
scheduleResult
后,在
sched.schedulingCycle
中的
sched.assume
将 assumePod 的 NodeName 更新为调度的节点
host
,并且将 assumePod 添加到缓存中。缓存允许运行假定的操作,该操作将 Pod 临时存储在缓存中,使得 Pod 看起来像已经在快照的所有消费者的指定节点上运行那样。假定操作忽视了
kube-apiserver
和 Pod 实际更新的时间,从而增加调度器的吞吐量。

func (sched *Scheduler) assume(logger klog.Logger, assumed *v1.Pod, host string) error {
	assumed.Spec.NodeName = host

	if err := sched.Cache.AssumePod(logger, assumed); err != nil {
		logger.Error(err, "Scheduler cache AssumePod failed")
		return err
	}
	...
	return nil
}

// kubernetes/pkg/scheduler/internal/cache/cache.go
func (cache *cacheImpl) AssumePod(logger klog.Logger, pod *v1.Pod) error {
	...
	return cache.addPod(logger, pod, true)
}

继续如
调度框架
所示,在
sched.schedulingCycle
中执行
Reserve

Permit
插件,插件执行通过后调度周期返回 Pod 的调度结果。

接着,进入绑定周期。

2.3 绑定周期

绑定周期是一个异步的
goroutine
,负责将调度到节点的 Pod 发送给
kube-apiserver
。进入绑定周期查看绑定逻辑的实现。

// kubernetes/pkg/scheduler/schedule_one.go
func (sched *Scheduler) scheduleOne(ctx context.Context) {
	...
	// 调度周期返回调度结果
	scheduleResult, assumedPodInfo, status := sched.schedulingCycle(schedulingCycleCtx, state, fwk, podInfo, start, podsToActivate)
	if !status.IsSuccess() {
		sched.FailureHandler(schedulingCycleCtx, fwk, assumedPodInfo, status, scheduleResult.nominatingInfo, start)
		return
	}

	// 绑定周期绑定调度结果
	go func() {
		...
		status := sched.bindingCycle(bindingCycleCtx, state, fwk, scheduleResult, assumedPodInfo, start, podsToActivate)
		if !status.IsSuccess() {
			sched.handleBindingCycleError(bindingCycleCtx, state, fwk, assumedPodInfo, start, scheduleResult, status)
			return
		}
		...
	}()
}

func (sched *Scheduler) bindingCycle(
	ctx context.Context,
	state *framework.CycleState,
	fwk framework.Framework,
	scheduleResult ScheduleResult,
	assumedPodInfo *framework.QueuedPodInfo,
	start time.Time,
	podsToActivate *framework.PodsToActivate) *framework.Status {
	...
	// 运行 Permit 插件
	if status := fwk.WaitOnPermit(ctx, assumedPod); !status.IsSuccess() {
		...
	}

	// 运行 PreBind 插件
	if status := fwk.RunPreBindPlugins(ctx, state, assumedPod, scheduleResult.SuggestedHost); !status.IsSuccess() {
		...
	}

	// 运行 Bind 插件
	if status := sched.bind(ctx, fwk, assumedPod, scheduleResult.SuggestedHost, state); !status.IsSuccess() {
		return status
	}

	// 运行 PostBind 插件
	fwk.RunPostBindPlugins(ctx, state, assumedPod, scheduleResult.SuggestedHost)
	...
}

可以看到,绑定周期运行一系列插件进行绑定,进入 Bind 插件查看绑定的行为。

func (sched *Scheduler) bind(ctx context.Context, fwk framework.Framework, assumed *v1.Pod, targetNode string, state *framework.CycleState) (status *framework.Status) {
	...
	return fwk.RunBindPlugins(ctx, state, assumed, targetNode)
}

// kubernetes/pkg/scheduler/framework/runtime/framework.go
func (f *frameworkImpl) RunBindPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (status *framework.Status) {
	...
	for _, pl := range f.bindPlugins {
		status = f.runBindPlugin(ctx, pl, state, pod, nodeName)
		if status.IsSkip() {
			continue
		}
		...
	}
	...
}

func (f *frameworkImpl) runBindPlugin(ctx context.Context, bp framework.BindPlugin, state *framework.CycleState, pod *v1.Pod, nodeName string) *framework.Status {
	...
	status := bp.Bind(ctx, state, pod, nodeName)
	...
	return status
}

// kubernetes/pkg/scheduler/plugins/defaultbinder/default_binder.go
func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status {
	...
	logger.V(3).Info("Attempting to bind pod to node", "pod", klog.KObj(p), "node", klog.KRef("", nodeName))
	binding := &v1.Binding{
		ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
		Target:     v1.ObjectReference{Kind: "Node", Name: nodeName},
	}
	err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
	if err != nil {
		return framework.AsStatus(err)
	}
	return nil
}

在 Bind 插件中调用
ClientSet
的 Bind 方法将 Pod 和 node 绑定的结果发给 kube-apiserver,实现绑定操作。

3. 总结

本文从源码角度分析了
kube-scheduler
的调度流程,力图做到知其然知其所以然。


记一次Flink CDC引起的Mysql元数据锁事故,总结经验教训。后续在编写Flink CDC任务时,要处理好异常,避免产生长时间的元数据锁。同时出现生产问题时要及时排查,不能抱有侥幸心理。

1、事件经过

  1. 某天上午,收到系统的告警信息,告警提示:同步Mysql的某张表数据到Elasticsearch异常,提示连不上Mysql,当时没有太上心,以为可能是偶尔网络异常。

  2. 然后立马大量用户开始投诉系统使用有问题,同时听到有同事反馈内部系统数据导不出来。此时我慌了。

  3. 立马看了微服务网关、用户中心服务、部分流量比较大的BFF层服务,CPU、内存、磁盘等都是正常的。但是Pod出现了健康检查失败的情况。

  4. 于是又赶紧看了日志,出现了大量拿不到Mysql Connection异常。

  5. 又赶紧看了Mysql情况,CPU、内存、磁盘都是正常的,但是出现了许多奇怪的慢SQL。

  6. 此时我大概猜测到了可能是什么操作锁表了,导致大量Connection无法释放,又赶紧看了Mysql锁的情况,果然发现了大量的元数据锁,高达400多个Connection没释放。

2、处理步骤

  1. 既然出现了元数据锁,导致这么多Connection没有释放,那就找出占用时间最长的那个会话kill掉。陆续kill了几个会话后,系统恢复了。
  2. 系统恢复后,又去看了慢SQL,发现主要有两块高频慢SQL,一块是Flink相关的,另一块是Nacos相关的。后来经过分析:元数据锁是因为Flink CDC执行
    FLUSH TABLES WITH READ LOCK
    导致的,跟Nacos无关,Nacos只是个烟雾弹。
# Flink相关的:
SHOW CREATE TABLE `xxx_db`.`xxx_table`;
FLUSH TABLES WITH READ LOCK;

# Nacos相关的:
DELETE FROM config_info WHERE data_id='com.alibaba.nacos.testMasterDB';
  1. 防止事故再次发生,又把Flink CDC任务里的SQL方式换成了API方式。Flink CDC使用SQL方式时,会产生大量任务,占用更多的资源,也容易出现任务异常。

3、原因分析

3.1、元数据锁

  • 以上关于锁的截图,可以看到是元数据锁引发的Connection被耗尽,那什么是元数据锁:
    • 元数据锁(Meta Data Lock,MDL),用于锁定数据库对象的元数据,例如:表、索引、视图等的结构信息。通常用于保证并发的数据定义语言(DDL)操作的一致性,防止在修改表结构的过程中出现并发问题。
    • 其作用是用于解决DDL操作与DML操作的一致性;通常,DDL操作需要获取MDL写锁,并且MDL锁一旦发生,就可能会对数据库的性能影响,因为后续对该表的任何Select、DML、DDL操作都会被阻塞,造成Connection积压。
  • 为什么要有元数据锁:
    • 主要为了保证元数据的一致性,用于处理不同线程操作同一数据对象的同步与互斥问题。比如需要事务隔离场景、主从同步场景。
  • 元数据锁和Innodb锁的区别:
    • 元数据锁主要关注数据库对象的元信息,而InnoDB锁主要关注数据的一致性和隔离性。
    • MDL锁还能实现其他粒度级别的锁,比如:全局锁、库级别的锁、表空间级别的锁。这是InnoDB存储引擎不能直接实现的。
  • 锁表的原理是数据库使用独占式锁机制。锁表发生在 insert、update、delete中。比如:A程序执行了对table_1的insert、update、delete,并还未commit时,B程序也对table_1进行insert、update、delete时会发生资锁表。

3.2、Flink CDC为什么引起元数据锁事故

笔者使用Flink场景是,利用Flink CDC同步数据,然后做汇总统计。

MySQL CDC如何工作

  1. 在 CDC 过程中,Flink 需要定期读取数据源的变化并进行处理。
    需要元数据锁
    确保在读取元数据(例如数据库表的结构信息)时,没有其他并发的操作修改了这些元数据,从而保证 Flink 的元数据和实际数据的一致性。
  2. 启动MySQL CDC源时,它将执行
    FLUSH TABLES WITH READ LOCK
    ,获取一个全局读取锁,防止其他会话对这些表进行写操作,从而保证捕获的数据的一致性和准确性。该锁将阻止其他写入操作。
  3. 然后,它读取当前binlog位置以及数据库和表的schema。
  4. 之后,将释放全局读取锁。然后,它扫描数据库表并从先前记录的位置读取binlog。
  5. 如果发生故障,任务将重新启动。

元数据锁原因

  1. 因为Flink CDC启动时执行
    FLUSH TABLES WITH READ LOCK
    直接上读取锁,由于时间较长,此时有大量的insert、update、delete操作一直处于等待,导致Mysql Connection无法释放。
  2. 正好此时,Flink CDC执行同步任务时,又出现了异常,然后任务重启,重启后是上锁,结果出现了恶性循环。导致更多的的insert、update、delete操作处于等待,导致更多的Myql Connection无法释放,直接Connection全部耗尽。
  3. 然后所有应用都拿不到Mysql Connection,所以系统彻底不可用了。
  4. 至于Nacos为什么会执行
    DELETE FROM config_info WHERE data_id='com.alibaba.nacos.testMasterDB'
    呢?查阅资料后发现,Nacos也是从Mysql获取Connection的,当Mysql出现问题时,比如死锁、Connection耗尽、CPU打满时,都会执行这个SQL。

======>>>>>>
关于我
<<<<<<======

本篇完结!欢迎点赞 关注 收藏!!!

原文链接:
https://mp.weixin.qq.com/s/36lqDS6Xli49LKyZQ56CcA

废话不多说,龙年腾云特效送给大家

预览

在线预览
龙年腾云
image

源码

龙是使用的
svg
,你也可以替换成其他样式的龙,而云是图片转化成的
base64
编码,所以整个文件就是一个
html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>龙年腾云</title>
  <style>
    .wrapper { margin:100px auto; width:200px; height:200px; position:relative }
    .dragon { position:absolute; z-index:3; width:100%; height:100%; top:0;left:0 }
    .dragon > svg { transform:rotateZ(55deg) scale(1.5) translate(0px,-40px) }
    #cloudContent { position:absolute; left:-30%; width:200%; height:100%; overflow:hidden }
    .cloud { position:absolute; top:0; right:0; height:20px; opacity:0; animation:6s linear 0s infinite cloud-left }
    .cloud > img { height:100% }
    .cloud:nth-child(1) { top:0; height:20px; z-index:2; animation-duration:6s; animation-delay:0s }
    .cloud:nth-child(2) { top:15%; height:42px; z-index:4; animation-duration:5s; animation-delay:1s }
    .cloud:nth-child(3) { top:35%; height:24px; z-index:2; animation-duration:5s; animation-delay:5s }
    .cloud:nth-child(4) { top:50%; height:36px; z-index:4; animation-duration:8s; animation-delay:4s }
    .cloud:nth-child(5) { top:70%; height:26px; z-index:2; animation-duration:7s; animation-delay:3s }
    .cloud:nth-child(6) { top:85%; height:32px; z-index:4; animation-duration:6s; animation-delay:2s }
    @keyframes cloud-left {
      0% { transform: translateX(0);opacity:0 }
      10% { opacity:1 }
      80% { opacity:1 }
      90% { opacity:0.2 }
      100% { transform:translateX(-400px); opacity:0 }
    }
  </style>
</head>

<body>
  <div class="wrapper">
    <div class="dragon">
      <svg viewBox="0 0 1024 1024" width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient id="RadialGradient" cx="0.45" cy="0.25" r="0.75"><stop offset="0%" stop-color="#fd091b"></stop><stop offset="100%" stop-color="#ffa731"></stop></radialGradient></defs><path d="M883.498892 483.297926c-0.75826-2.427029-10.318605-15.799571-0.811995-22.573162-4.319694 2.491212-12.002301 2.227016-7.015398 24.25835-3.549493-0.413461-6.470585 2.400162-3.115135 13.145661s7.946804 29.094496 8.508035 35.766587c-0.173146 3.097223-2.509124 5.348121-4.652552-2.73302s-6.645224-18.847538-9.134943-26.819716-7.230338-18.284813-11.88289-14.926378c-1.146346-0.02239-8.548337-9.124495-9.631991-18.677377-3.374854 2.576293-4.28387 8.185626 0.695569 24.129983-1.622497 0.888119-3.309178 5.224232 5.131689 20.081949a139.481032 139.481032 0 0 1 10.2992 25.694267c0.974692 3.119613-3.028562 8.554307-5.409319 0.928421s-11.451517-31.802141-14.568145-36.915918-9.739462-6.849715-11.426142-2.519573c-1.364271-0.714974-8.006509-1.303073-11.493311-17.334003-1.882216 2.48972-5.927265 10.220091 1.994164 22.202988-1.406065 1.580703-6.901957 7.100478 4.070423 15.468205 5.994433 4.594339 17.89822 18.377357 18.677377 25.742032 1.170228 4.960035 3.485309 9.943953 6.948229 8.861791s25.664414 1.877738 31.140902 9.678263c1.449351 2.209104 1.450844 8.296081-2.985275 6.257138s-19.389365-6.119815-26.682394-8.785666-26.379388-12.687421-38.24138-34.850108-28.942247-78.078391-83.708621-57.553128c-0.520931-2.880791-4.179386-9.727521 4.152518-13.469564s37.102498-16.24587 62.741538 18.398254c-1.826989-11.941102-20.944694-81.67117-113.706163-35.582993-16.963829 11.384349-30.68117 27.079435-42.602868 38.792164-3.764433 4.97944-24.42851 23.98669-24.628524 3.880858 1.253816-5.716803 14.132295-39.683269 64.929745-48.809256-1.232919-1.51652-98.845461-29.072107-145.382922 80.781558-2.249405 9.832005-14.014376 62.19971-25.942045 25.225579-0.476151-5.176468-1.841915-18.060917 14.060648-34.063487 8.358772-10.978351 56.632171-52.682651-11.997823-178.349321 7.443785-5.373496 14.48605-13.654651 35.027731-7.896054 17.195188 4.776441 36.965175 15.532389 12.875494 55.824654 8.74238-1.209037 52.85729-20.561086 55.417164-75.636436 0.807517-2.282243 3.228576-9.12748 5.682472-1.268742s5.682472 6.84673 5.166019 18.168388c3.08976-1.470248 23.688162-15.005488 5.013771-63.447556-12.29635-21.520852-29.49303-42.529729-61.347414-40.696769 3.895785 4.365966 31.282703 36.899499 2.170295 47.512154s-64.332689 19.517732-66.72091 16.717543c1.641902-3.824138 19.680429-30.00202 25.225579-39.07278 2.010583-3.291266 5.398871-0.355248 5.398871-0.355248 2.08223 8.133383 1.806092 11.941102-0.977677 25.673371 3.203201 0.522423 12.230674-7.879635 15.405514-13.941237 4.731662 0.552276 14.572623 6.104889 16.680228 7.982627 2.939004-1.940429 3.922652-5.282445 0.1418-7.654247s-11.04552-6.189969-10.648478-11.385841 4.760022-9.094642 6.17355-2.941989 5.821287 7.313925 7.445278 10.863418c0.204491-3.715175-10.273826-36.224827-15.805542-48.482369-0.617952-1.370242 3.134539-4.246666666 4.2779-5.085417 3.416648-2.51808 9.552882-6.269079 13.865112-3.433067 5.742178 3.785329 18.325114 12.442629 21.666666183 19.137109 2.212089-4.477913 0.22091-24.447915-6.300424-35.599411 1.440395-3.492772 9.490191-16.654853 19.852083-22.426883s104.932438-40.375853 53.217015-121.617143c-18.240034-22.710484-66.422382-63.459496-147.606952 23.776227-22.96274 33.547035-68.653876 104.626447-106.743008 96.23036-8.685659-1.852364-25.133035-11.932147-23.794139-34.41575 1.859827-7.428858 12.350085-46.844945 45.740393-47.00018 6.707914 3.231561 36.529325 15.983166-7.805003 55.315665 6.361622-2.746454 58.768136-38.133911 9.382721-59.993591-8.222942-1.997149-26.443571-3.153944-51.540783 32.073801-7.807988 18.796788-12.023198 53.970798 22.925424 68.544913 12.94117 1.285161 36.332297-4.118188 62.340018-32.782804s67.677691-78.96054 82.456297-90.04039 39.745959-39.422057 83.395167-17.674325c16.533949 11.575406 69.407658 61.638478-24.068784 115.176411-1.471741-7.148242-4.358502-16.390656-9.403619-22.80452-3.255443 7.293028-3.518147 14.286036-10.226061 17.141452-2.510617-6.82434-8.333397-24.661362-21.664145-29.62886 2.350905 13.214322 3.536059 19.440115 0.008956 23.9643-2.964379-0.976185-18.74156-14.180059-25.971898-14.209912 2.537484 5.672024 25.770392 28.981056 10.082768 45.674717-3.788315-12.13216-15.886144-27.763063-33.369411-13.165066-4.776441-1.931473-24.713604-15.868233-42.540177 11.002234-2.330008 4.451046-6.200417 13.59793 1.238889 27.466666679 0.920958 1.715041 2.606146 7.66171 2.125517 9.318538-1.464278 5.074969-3.453964 17.084732-3.201709 23.056776-2.21806 7.860231-8.609535 18.210181-23.694132 15.354766-0.959766-0.182102-4.068931-4.776441-5.101836-7.545285-3.849513-10.330546-13.732268-24.298651-46.188184-5.47201-10.926109 7.597526-47.234523 28.827314-61.431002 21.086495-8.202045-3.144988-35.085944-15.417456-8.71999-53.07223 0 0-30.439363 7.981134-46.995701 42.573015-3.404707 2.079244-88.777619 19.105764-116.528741 55.157445-7.355719 8.893136-30.599075 33.400756 18.644539 41.859535-1.992671-8.003524-9.581242-31.494658 38.627974-49.089873 35.457611-10.567876 78.272434-21.91043 103.247249-3.58233 8.772232 7.404976 27.857099 27.224221-21.243221 41.886402-10.009629 3.350972-53.318515-1.610556-27.89143-40.237037-4.948094 2.813622-45.122441 20.180463-44.340299 43.774588 0.404505 2.916614 2.931541 6.948229-3.880858 11.10224s-60.529448 26.000258-87.734265 49.207791c-10.933572 12.039617-21.419352 24.692707 14.072589 45.05975-0.131352-5.283938-11.861993-28.251156 28.557147-46.452381 27.364529-11.335091 159.979427-63.653539 172.825068-16.435435 8.657299-5.11527 23.547854-21.941776 26.619703-26.700305 2.337471 3.834587-0.149264 38.468261-34.768013 55.366414-14.541277 4.540604-27.934717 9.105091-39.081735-14.424852 1.641902-2.035958 1.492638-9.215546 8.005016-4.776441s30.3304 19.255028 25.609187-7.427365c-2.304633-5.557091-5.022726-22.777653-31.528988-7.272132-14.898018 8.266228-48.565956 29.431832-73.385538 34.520235 5.854125 5.970551 29.206444 27.209295 68.697162 19.82223 4.197298-0.549291 12.615775 3.283803 2.403147 6.478048s-33.882878 5.266666678-39.784768 4.627177c-5.322746-0.619445-12.463526 0.849311-14.269617 8.451315-4.337605 11.055968-17.41162 56.794868-14.086023 84.4833 7.951282-1.913562 62.786317-19.987913 81.647288-22.071635 15.766733-4.671956 48.933145-11.221651 93.561523-62.187769 2.307618 3.336045 14.254691 18.878883-10.235018 60.513029a55.196253 55.196253 0 0 0 41.346067-38.77873 394.084741 394.084741 0 0 0 16.065261-43.059615c0.789605-2.338963 5.731729-21.195457 20.862599 19.353541 0.779157 1.279191 26.916738-16.014511 4.209238-71.692886 2.813622-3.161407 9.033444-10.620118 11.344048-14.766666 3.636066 4.95108 68.026968 75.066248 25.53754 150.725073-13.763613 3.789807-40.711696 5.104821-62.316136 43.807427-4.627177 2.207611-29.127334 7.313925-35.402383 3.446501 3.731595 4.667478 15.886144 9.496162 29.488552 6.007867-0.967229 2.585249-14.868165 30.257261-17.08921 46.676277-1.801614 6.395953-15.663741 27.713806-26.603284-0.822444 1.386661-4.489855 3.692786-21.440249 26.585372-30.872228-3.731595-0.610489-30.945367 2.307618-60.06673 38.032412-2.753917 4.151026-24.159836 36.197959-50.851185 19.689385-7.355719-3.283803-31.334945-13.553151-45.132889-56.923235 0.191058 14.396492 2.122531 58.296462 40.151957 86.372979 25.904729 13.59793 78.124663 43.316349 191.460651-47.615146-4.5212 8.642373-113.041939 124.321803-209.390217 57.526261-9.954402-7.537821-74.63189-83.214558-16.898152-143.340994-10.037989 5.166019-65.377536 43.749214-45.823981 127.429475 8.773725 21.598469 25.743524 62.959463 106.0146 81.527877 31.912596 3.224098 132.958205 15.284611 108.11922 50.943728-3.721146 2.686748-18.926647 5.910846-14.827864 23.897131-2.047899 3.179319-8.193089 8.642373-0.904539 27.927254 0.286586-3.134539 2.016554-17.866875 7.463189-23.12096 5.224232-1.134405 19.404291-2.76138 31.173741-10.493244 2.337471-2.253883 2.221045-4.254018 6.779561-2.627042s25.015117 7.91098 36.169599 16.851881c3.015128 2.358368 3.722639 1.373227 6.146682 6.701943s12.75459 17.329525 21.410397 8.537889c1.238889-0.895583 1.641902-6.104889 5.712325-2.806159s11.166423 4.11968 14.357683 21.643248c4.030122-2.522558 11.344047-13.941237-9.975299-31.121499-0.462718-3.910711-0.298528-12.329188-9.982761-18.195254s-24.355371-14.702482-32.809672-16.627986c-2.510617-0.746319-5.785464-5.552613 1.312029-4.731661s45.183639 5.179453 49.397355 8.925974c4.573442 0.850804 17.184739 1.477711 19.3013-9.582735 0.404505-1.134405 1.400094-5.254085 4.428656-3.656963s15.480147 2.522558 21.137244 14.956231c0.820951-3.044981-0.149264-16.702617-18.586326-24.628524-0.101499-2.76138-0.995589-12.120219-15.451786-12.164998a234.226217 234.226217 0 0 0-40.363912 2.955423c-1.832959-0.179117-7.463189-1.970282-3.968924-4.582398 3.925637-1.223963 48.025621-11.70228 51.184043-17.001145 1.716533-1.806092 6.533276-9.896189 6.269079-12.359041 1.325462-1.432932 5.203335-12.523231 21.998496 1.507565-0.149264-4.522693-1.167243-11.060446-22.850792-13.165066-3.24798-2.283736-10.794757-11.851544-26.315205 0.104485-10.327561 8.299066-37.345798 33.733614-62.690788 35.300884-6.71687-0.432865-14.411418 1.716533-20.947678-4.612251-5.339165-4.925705-10.069335-9.537956-18.120623-12.597863-0.746319-0.791098-10.448465-5.880993 8.696107-15.150274 14.339771-6.000404 42.425244-15.030863 102.227778-79.856122 14.424852-10.597728 90.473255-76.951449 143.160385-47.753961 6.664628 3.49725 37.687612 37.932405 3.385302 64.381946-2.800189 2.38822-2.997217 4.98541-13.156109 0.044779-10.387266-8.925974-22.823925-14.687556-28.481022-23.061254-8.081141-9.657367-25.309167-29.125841-37.4264-19.255028-3.49128-1.444873-15.795093-5.970551-26.52716 7.522895 4.370443-0.597055 20.989473-5.791435 24.683752 1.179184 0.244793 3.209171 0.089558 7.567674 5.858603 9.836483s28.189958 14.015869 32.258888 17.314598c0 0 0.025375 2.522558-3.016621 1.701607s-9.552882-4.627177-18.88336-8.567741-24.038932-11.254489-31.92603 1.612049c-2.452404 0.253748-14.62785 1.014994-16.226466 12.926244 3.606213-0.626908 8.306529-6.657165 19.371453-5.298865a34.527698 34.527698 0 0 0 19.723716 8.806564 233.550052 233.550052 0 0 1 40.813196 9.836483c2.032973 1.641902 6.17355 5.164527-1.761313 4.089827s-30.957308-5.030189-37.13235-6.149668-16.214524-4.074901-15.808527 11.015667c-1.401587 1.209037-12.956096 1.522491-15.593588 19.837157 3.186782-2.776306 9.273759-13.284476 17.410128-10.761919 2.612116 3.492772 8.300559 10.34398 29.172113 2.552411 4.601802-0.671687 7.463189-2.074767 10.472347 0.283601s27.515285 21.583543 37.324901 20.553623c6.346696 0.044779 8.409521-3.895785 33.152978 18.553488 3.283803 0 4.925705 2.776306 3.709205 14.314396 2.482257-1.791165 6.791502-7.448263 4.830176-16.971292 1.746386-3.343509 4.874955-11.164931-6.536261-22.568683-2.786755-2.418073-10.415627-7.388557-8.508035-19.150543 1.225456-4.179386 13.900936-23.61353 21.925356-6.836281a132.71789 132.71789 0 0 1 10.424583 52.033353c2.38822-5.313791 13.300895-97.737923-22.194032-138.397377-18.92814-18.43557-51.133293-38.808583-101.76506-14.380072-49.534678 23.077673-71.231661 29.852756-131.202863 87.638736 2.567337-6.373563 48.718205-85.716219 153.710348-112.925513-15.232369-3.359928-66.859725 0.074632-119.617008 40.398242 5.224232-9.239428 62.659442-70.292792 145.900868-55.200732 22.101488 4.258496 118.79158 28.339221 102.653179 171.342879-14.275588 59.750291-21.252177 105.544419-86.332678 182.683941 14.712931-9.672293 63.928185-48.376391 91.186736-115.216712 0.289572 0.925435-2.153876 17.419083-14.70696 42.137165s-69.140476 138.75561-251.227361 157.577773c27.813813 1.462785 97.697622 2.970349 149.453345-32.479798 0.92245-0.283601 10.718632-4.612251 3.49128 1.447858s-23.400083 15.926445-69.330041 34.33067c18.265409-3.164392 165.133506-28.479529 213.715881-172.235476-5.672024 8.61252-34.876975 40.808718-29.310929 13.209844 7.125853-12.881464 61.672809-99.409678 54.182753-197.998405-0.088066-3.537552 5.391408-18.431092 25.330063-4.880925 3.695771 6.970619 2.937511 31.300615 13.091926 32.181271 1.615034 3.552478 5.424246 12.508305 14.008406 11.612722-2.612116-3.507699-5.695906-10.149937-5.627244-13.97109 2.238957-0.955288 8.579682-9.030459 4.785396-22.807506s-14.67263-29.145246-12.064991-41.877446-0.535857-55.247003-4.562994-73.009393c-1.852364-12.002301-6.592981-16.233929-9.751402-12.959081zM574.151199 182.613503c0.806024-3.74055 10.978351-10.275319 12.926244-4.030122a112.097099 112.097099 0 0 1 4.509258 18.501246c-1.213515-0.637356-6.697466-5.030189-12.309784-8.487139 0-0.005971-5.586943-3.846528-5.125718-5.983985z m-47.542006-20.223749c-1.356808-7.119882 1.689666-10.597728 7.418409-6.055632 5.224232 5.137659 14.977128 23.526957 3.777867 25.413651-4.171923 0.709003-11.42465-16.326472-11.196276-19.358019z m-26.854047 38.808583c0.311961-0.959766 8.160251-9.345405 11.303746-10.72311 13.109838-5.74367 15.915997-5.139152 22.408971-4.289841 11.224636 3.08976-9.80663 13.208352-24.376268 19.280402-11.085821 4.618221-10.332039-1.210529-9.336449-4.267451z m-127.151844 112.32398c-25.61665 28.288472-19.229653 5.748148-18.657973 3.542029 14.733828-41.96999 35.421788-61.786249 42.216275-69.234512 31.696164-32.342476 60.590646-0.661239 60.590647-0.661238-19.974479 0.531379-23.39262 5.403349-27.351096 8.542366 7.28258 22.155223-5.52276 23.28515-12.851611 22.528382-8.079648-1.532939-10.712662-4.898837-12.930721-5.918309-3.134539-0.92245-14.823386 9.509595-16.671272 14.147221 9.788719-7.745298 15.335361-7.866201 17.972852-6.831803 1.356808 0.529886 8.252794 3.524118 9.878277 3.843543 18.807236 3.680845 20.099861-9.316666663 22.625404-17.087718 2.836012-8.74835 5.580973 0.031345 4.674941 3.613676-8.385639 28.210855-25.792781 25.300211-35.048628 23.37023-11.947073-4.891374-22.546294 9.581242-34.447095 20.146133z m120.088682 24.379253c-3.591287 7.969193-11.770942 15.848828-13.630769 17.19071 1.492638 6.001897 20.216286 35.533736 8.131891 49.769022-1.331433 0.606011-3.244995 1.774746-6.28699-1.270235s-19.823723-19.677444-25.225579-3.533074-13.333734 53.42897-25.764421 61.686243c-1.044846 0.323902-2.855416 2.413595-3.040504-3.044981s0.8717-31.278225-13.881531-43.22082c-0.9732 2.967364-34.927725 68.038909-121.948509 78.979944 0.100007-1.300088-1.558314-2.666666396 2.450911-4.316709s55.755993-16.14437 82.841399-69.746487c-6.90942 5.457084-92.782366 57.37849-116.401867 27.980989 5.09288-2.350905 75.043858-22.929902 113.41062-72.442191 0 0 18.90575 56.482907 59.214435 17.645964 24.777788-24.094159 44.982133-49.427208 51.581084-58.715893 3.447993-4.497318 12.139623-4.931675 8.54983 3.037518z m-174.544587 126.015947a12.218733 12.218733 0 0 1 2.49569-3.122599c11.388826-9.339435 25.086764-15.129377 26.718217-14.550233 6.330277 5.061535-15.665234 18.587819-24.612105 21.571602-6.38849 2.129994-5.628737-1.525476-4.601802-3.89877z m137.958541-145.211269c-0.195536-1.343374 2.019539-4.109232 3.56442-5.464547a155.031333 155.031333 0 0 0 7.290043-6.866134c6.615371-6.630297 9.497654-0.174639 12.139623 5.848155s-10.243973 11.820199-16.201091 15.454772c-4.467465 2.724064-6.206388-4.960035-6.792995-8.972246z m22.117907-33.733615c-10.012614 6.170565-27.049582 20.240169-33.47688 20.344654 3.074834 7.409454 22.500022 61.831028-14.654718 66.965702 1.492638-6.172057 10.967903-34.238126-7.015398-48.024129 0 0 8.293096 35.076988-7.504983 27.071972-2.35986-2.686748-14.478587-11.076865 5.706355-41.346067 8.914033-10.393237 32.023051-31.305093 51.260167-35.409847 3.568897-0.731393 17.429532 3.689801 5.685457 10.400701z m132.493995-42.264039c0.326888-2.38822 4.32865-5.789942 3.531581 0.165683s-12.412776 62.462414-50.313835 67.640375c-1.212022 0.377637-5.639186-1.632946-1.132912-3.625618 4.410745-1.95237 41.793859-24.747935 47.809189-63.458003 0.037316-0.237329 0.071647-0.477644 0.105977-0.719452z m-4.986903-4.401789c4.01221 6.355652-2.300155 12.51129-4.983917 14.871151-1.125449-7.257205-20.092397-31.896177-20.092398-31.896178 9.34242 1.522491 21.064105 10.67236 25.076315 17.028012z m-34.018708 37.886133c1.922517-1.885202 12.721752-13.608379 17.4967-19.404292s5.237666-1.888187 9.09315 3.755477-8.817012 18.222122-12.769517 25.036014-10.976858-1.898635-14.415895-4.795845 0.595562-4.588369 0.595562-4.588369z m1.532939 18.711707c1.070221 2.619579-15.639859 11.564958-19.016206 12.114249-0.376145-8.508035-14.963694-30.073666-14.963694-30.073667 2.177759-10.058886 31.221505 15.614484 33.9799 17.962404z m-62.123585-1.813666666c13.732268-2.262839 48.062937-1.822511 34.408286 12.836686-5.509326 1.264264-16.316024 2.553903-31.706612 1.444873-1.768776-0.128367-7.893069-2.079244-9.521536-4.268944-2.306125-3.092746-3.044981-7.375123 6.819862-10.009629z m-2.203134 23.762794s15.427904-9.003591 13.502402-4.216702c-1.082162 2.619579-3.907726 6.548202-10.399208 10.097695-3.852498 1.456814-8.6666668-0.632878-3.103194-5.878007z m-88.79553-45.43888a4.901823 4.901823 0 1 0-6.136234-3.21962 4.901823 4.901823 0 0 0 6.136234 3.21962z m-228.970639 542.346961c13.154617-8.164729 24.785251-0.134337 39.211595 5.507834s19.820737 3.447993 23.861308 0.16419 16.157804 3.059907 26.285351 9.537955 11.830647 5.433202 15.205502 4.895852c6.984052 2.880791 5.916816 10.821624 8.110994 16.224973 4.586876-8.015465-0.753782-17.001145-2.485242-22.553757a32.458902 32.458902 0 0 0-12.439644-30.106504c-14.226331-8.239361-8.715512-16.553353-8.196074-19.747599 13.583004-23.016475 19.631172-19.075911 25.386784-20.105831 42.284936 8.866269 77.599254-2.164325 91.318088-11.776912-87.728294 9.896189-138.801882-38.062264-152.089344-51.421372-38.489158-47.495735-35.068033-102.897972-30.785654-128.134 1.083655-6.381027 0.667209-25.937567-0.017912-31.769303-6.070558-51.758709 5.785464-64.814811 12.16052-82.304049 7.096-19.459519-14.224838-8.239361-19.649084-4.516722-12.635179 13.077-27.897401 19.36399-47.891284 25.100198-18.43557 4.236106-31.942449 22.655257-40.013142 37.350275-12.687421 24.253872-1.928488 41.686389-1.928488 41.686389 16.76083-31.024477 32.97834-39.720585 44.611959-45.698599a25.856965 25.856965 0 0 1 5.418275-1.864305c11.179857 0.883642-15.381633 32.030515-33.411204 54.545464-20.913348 30.370701-8.673718 71.182404-3.680845 82.302556 8.657299-53.417029 25.325586-53.554352 28.061591-52.888636 13.583004 1.343374-5.160049 38.633944-9.976791 50.798943-11.879904 46.316551 26.576416 71.70632 34.279919 76.915626-8.806563-13.612857-12.491886-43.659656-12.753097-54.227532s8.027406-5.925772 11.599288-0.567202c7.358704 11.388826 5.780986 28.240707 9.440934 41.181877 7.988598 32.882811 42.765566 44.46568 46.086685 44.182079-18.22063-16.299605-16.107055-23.344855-13.371049-24.703156 3.589794-1.791165 11.778405 6.000404 14.551726 7.463189 6.01533 3.164392 8.557293 3.507699 16.335428 7.5826 11.361959 5.970551 7.186666668 14.493513 4.806294 19.046059-11.141049 19.075911-19.755061 13.403887-23.412024 12.642642-10.884315-0.791098-46.868827-27.210787-60.141363-35.629264s-23.631442-6.313858-25.447982-1.179184c-18.807236-0.597055-23.217981 8.388624-27.464535 14.284544 9.932012-9.567808 22.720933-6.328784 26.334609-3.283804 1.73146 5.552613 6.203403 9.731998 19.014712 11.821692s31.667804 8.11995 34.221707 10.612655c-5.769045 1.806092-27.366022-4.895852-42.84766-7.41841s-21.479058 7.478115-21.017833 11.388826c-16.504096 7.179588-15.232369 16.926513-15.02937 22.449273 6.186984-15.881666 17.093688-14.224838 19.459519-13.941237 3.318134 6.567606 20.001347 6.433269 33.036552 4.388355s41.644595 1.955356 42.731235 5.418275c-5.52276-3.08976-35.142664 1.850871-44.792568 2.582263-22.462706-1.3583-19.502806 12.179924-18.23257 17.866875-12.763546 15.896593-11.782883 20.673034-9.761852 27.136155 5.566046-19.478923 12.951618-17.732537 15.865248-18.135549 3.87041 5.895919 16.277215 5.060042 29.440788-3.104687z m483.947507 66.855247c-1.374719 1.298595-10.712662 5.134674-14.878614 6.298932-14.926378 4.179386-43.031255 11.53809-59.202493 22.538831-20.686467 14.075574-31.114035 27.73321-74.348289 40.972908 17.251908-2.343441 76.109602-8.552815 126.29507-41.719227 26.718217-17.478789 95.715399-63.392328 666666.56572-170.011446 0 0-53.649881 18.016138-74.696074 88.11041 6.081006-6.075036 25.80323-25.67337 33.373889-29.807977 5.637693-3.030055 18.795295-5.119748 10.632059 7.075103-5.682472 6.328784-5.136167 5.657097-23.116482 16.210046s-46.319536 23.98669-63.138579 64.481954c4.045048-3.030055 25.225579-21.777586 33.239551-26.359984 5.068998-3.224098 21.593991-3.447993-5.733222 22.21045z m-195.731088 102.0815c-2.141935-2.000135-18.675884-13.568078 13.608379-22.135819-5.821287-1.597122-47.368861-6.88106-63.884899-19.61326-2.964379-0.970215-16.102577-16.269752 11.76945-15.836888a195.237025 195.237025 0 0 1 67.777697 19.911789c-2.403147-6.478048-15.908534-26.598806 5.103329-27.464536 10.193224 1.000067 37.739854 6.478048 46.20311 14.105427-3.940564-10.17979-51.366145-106.245959-188.488809-56.018696 13.244175 3.46292 51.806473 8.925974 59.965231 14.359175 4.30626 2.850938 12.705333 13.911384-11.642575 15.060716-10.084261-0.656761-23.583677-2.537484-55.702257-15.329391-22.025363-7.567674-41.304273-18.120623-68.901654-12.135145-6.542231 1.40308-33.750033 11.030593-39.39519 12.732201-14.999517 4.522693-35.52478 8.149802-56.056012-3.671889 0 0 44.891082 70.810737 101.12024 64.661069 30.348312-2.627043 58.327807-7.940833 76.160352 7.79157 3.030055 3.62711 12.639657 11.284342-1.641902 14.209912s-49.273467 2.836012-64.595394-6.075036c10.345473 11.224636 60.816035 60.899622 139.604922 56.451561 28.260112-1.970282 76.47082-15.508507 113.774824-40.853496-12.032153 5.283938-60.699609 12.493378-84.786306-0.149264z" fill="url(#RadialGradient)" stroke="#fff" stroke-width="2px" p-id="2374"></path></svg>
    </div>
    <div id="cloudContent">

    </div>
  </div>
</body>
<script>
  document.body.onload = function () {
    let imgs = '';
    let img = '<div class="cloud"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABmCAYAAAA9BvYaAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGW2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDEtMzFUMTA6MzU6NDQrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDEtMzFUMTA6MzU6NDQrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDI0LTAxLTMxVDEwOjM1OjQ0KzA4OjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjAyZTI3ZTQ3LTNhZmMtNGZhZS05MjJlLTY0NzU0YjRkYWNkMSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmQ1MTA1NDdiLWIwYzEtM2U0Ni05NTA4LWQyYTcxYWIwNjZkZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmFjZjYxMzVjLTFhY2MtNGNiNS1iZGY4LTlkN2I4YWE0NjczYiIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmFjZjYxMzVjLTFhY2MtNGNiNS1iZGY4LTlkN2I4YWE0NjczYiIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0zMVQxMDozNTo0NCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjAyZTI3ZTQ3LTNhZmMtNGZhZS05MjJlLTY0NzU0YjRkYWNkMSIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0zMVQxMDozNTo0NCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDxwaG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDxyZGY6QmFnPiA8cmRmOmxpPjVCODE1OTFBRTMxOTBDMjFCRUYyNDJBMDM4OTlFRjVBPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Z830YgAAQGRJREFUeJztvUmTHEl69/dzjy3X2ncUCntPb8OZIYd8+UonSRcdZLpIZ9110FUfQKYPoYtOushMprNeM8lI2kujaBoOOT3TPb2gsTZQqH3NNSJ80cHDIyMTVSgMgG4sU3+zBCqrMiM8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"></div>';
    for (let i = 0; i < 6; i++) {
      imgs += img;
    }
    document.querySelector('#cloudContent').innerHTML = imgs
  }
</script>
</html>

说明

svg

1. svg里的
<defs><radialGradient>
这两个标签是用来控制svg图形的渐变色:

  • defs 是定义容器,其标签里的内容并不会展示在 svg 中;
  • radialGradient 用来定义径向渐变,以对图形元素进行填充或描边。横向渐变是 linearGradient 标签。
  • radialGradient 需要定义 id,以方便 svg 图形来引用此定义。

2.
cx="0.45" cy="0.25" r="0.75"
指的是径向渐变的起始位置和半径,数值是百分比的形式:

  • cx
    是 x 轴方向,
    cy
    是 y 轴方向,
    r
    是半径;
  • 如果你的起始点不在正中心,那么半径肯定要大于 0.5,才能让你的整个渐变都覆盖到图形上。举个例子,如果
    cx="0.2" cy="0.5"
    ,那半径等于
    1-0.2
    也就是0.8,则刚好你的渐变覆盖整个图形,当然大于 0.8也可,但是你的渐变色将不是100%的表达出来。

3.
<stop offset="0%" stop-color="#fd091b">
用来定义渐变的颜色:

  • offset
    位置偏离量,
    stop-color
    当前位置的颜色
  • 设置多个
    stop
    标签,来表达渐变过程,如果只有一个那就不是渐变了,是单色。

4. 定义好以上 内容后,需要在 path 里引用此定义:

  • path

    d="xxx"
    的内容,是整个图形的绘制内容;
  • fill="url(#RadialGradient)"

    fill
    指填充颜色,
    RadialGradient
    是此前我们说的
    radialGradient
    标签的 id;
  • stroke
    是描边颜色;
  • stroke-width
    是描边的宽度。

云朵动画

由于 base64 编码太长,所以直接在 js 遍历生成了,如果你用图片或者不介意长度,可以将 n 朵云朵直接复制在 hrml,放在元素
div#cloudContent
里,完全不用 js 了。

  • 每个云朵的动画名称都是一样的,左右往左移动。
  • 不同的是云朵距离顶部的位置
    top
    ,以及它的层级
    z-index
    ,如果
    z-index
    值比图形龙所在元素的层级大,那云朵就漂浮在图形龙的上面,反之在图形龙的下面;
  • 还有就是动画延迟开始时间
    animation-delay
    和 执行整个动画需要的时间
    animation-duration
    ,这些数值也都可以调整;
  • 为了避免云朵消失和出现的太过突兀,所以在动画
    @keyframes cloud-left
    里设置了透明度,达到了渐隐渐现的效果。

广告时间

如果你的博客是采用
hexo+hexo-theme-butterfly
的框架和主题(其他主题也可尝试,但可能要自己处理下兼容性的问题),不妨尝试下个人开发的首页导航插件
hexo-butterfly-recommend
,最新版本 1.1.0 已集成此特效。

在线效果预览
weizwz的博客

截图展示:

image

image

当然,最后祝大家
龙年快乐

XPath 通常用来进行网站、XML (APP )和数据挖掘,通过元素和属性的方式来获取指定的节点,然后抓取需要的信息。

学习 XPath 语法之前,首先了解一下一些概念。

概念介绍

节点之间的关系

以上面的 HTML 节点树为例,节点之间包含了下列的关系:

  1. 父节点 (Parent): HTML 是 DIV 和 P 节点的父节点;
  2. 子节点 (Child):DIV 和 P 是 HTML 的子节点;
  3. 兄弟节点 (Sibling):拥有同样的一个父节点,DIV 和 P 就是兄弟节点。类似的 span、img 和 i 也是兄弟节点。
  4. 祖先节点 (Ancestor):html 是 span 的祖先节点,隔开一级;
  5. 后代节点 (Descendant):span 是 HTML 的后代节点,隔开一级。

除了了解这些概念,parent、sibling 等关键词也非常关键,在匹配复杂的结构时常常用到。

绝对和相对路径

xpath 中绝对路径使用
/
开始,比如:
/html/body/div[1]/a/img
,绝对路径较长,其中可能包含变化的部分,不建议单独使用绝对路径来选择元素,最好配合其它语法。
比如下面的情况单独使用绝对路径进行定位就会出错:

// 本意是匹配第三个div下的span,但因为第一个div因为是动态显隐的,导致匹配第而个div匹配到之前的第三个了

/html/body/div[2]/span

相对路径以
//
开始,比如
//*[@class]
,表示只要包含 class 属性的元素均可匹配,无论从哪一个节点开始。

下面是一些常见选择节点示例:

表达式 说明 举例
/ 下一个节点,或者根节点开始 /html/body/div
// 从任意节点开始 //img
. 选取当前节点 //a/.
.. 当前节点的父节点 //a/..
@ 选取包含某属性的元素 //div[@class]或//@class
* 表示任意元素或者任意属性 //*[@class]

除此之外,通过谷歌浏览器-元素上审查元素-复制 xpath,可以直接获取绝对路径和相对路径。

但复制下来的代码,通常还需要进行一些修改,才能具备通用性。

基础语法

定位需要的信息通常通过元素、属性名、属性值以及三者结合等方式进行。

下面来分别看一下,也这段 html 代码为例:

<div id="app">
  <p class="title">喜欢的动物</p>
  <ul>
	<li class="cat">猫</li>
	<li class="dog">狗</li>
	<li id="panda">熊猫</li>
  </ul>
  <p class="title">喜欢的电影</p>
  <ul>
	<li>阿甘正传</li>
	<li>霸王别姬</li>
	<li>阿凡达</li>
  </ul>
  <p>其它不需要信息</p>
</div>

1. 通过元素名定位

示例:

1.1
//div/p

定位所有 div 下的 p 子元素,可以是任何 div,只要这个 div 的子节点包含 p 就可以匹配

1.2
//ul

会定位从任何节点开始的 ul 元素

1.3
/html/body/div/p

使用绝对路径定位元素,必须从
/html
开始,否则最好使用
//
相对路径开始

2. 通过属性名定位

通过元素是否包含某个属性来进行定位,属性名需要使用
@
开始,同时放在
[]

2.1
//*[@class]

定位包含 class 属性的元素

2.2
//@class

这种语法定位到的是属性里面的具体值
title
,而不是元素,所有没有元素没选中

3. 通过属性值定位

示例:
//li[@class="cat"]

定位包含 class 属性,值为
cat
的 li 属性元素

4. 使用逻辑运算符定位

常用逻辑运算符包括:
and

or

not
三种

示例:

4.1
//li[@class and @class="cat"]

选中包含 class 属性,并且属性值为 cat 的 li 元素对象。

4.2
//li[@class or @id]

选中包含 class 或者 id 属性的 li 元素对象。

4.3
//li[not(@class)]

选中不包含 class 属性的 li 元素对象。

5. 使用谓语定位

5.1
//li[1]

定位任意元素下的第一个 li。

注意
xpath 中索引从 1 开始。

5.2
(//li)[1]

两者区别如下:
//li[1]
任意元素下第一个li,也就是说这个 li 在任意的 ul 下是第一个就会被选中
(//li)[1]
将所有的 li 选出来的结果数组中取第一个,这两者是完全不同的含义

6. 使用文本定位

使用元素中文本的内容进行定位。

示例:

6.1
//li[text()="猫"]

选中文本内容为

的 li 元素对象。

6.2
//*[contains(text(),"喜欢")]

选中任意元素文本中包含
喜欢
两个字的元素,其中
*
表示所有元素是通配符,
contains()
表示包含函数。
类似的有:
starts-with

ends-with
函数,表示以什么字符开始和字符结尾的文本。

节点选择器

除了相对和绝对选择之外,下面这些选择器在处理较复杂的匹配场景可以发挥关键作用。

  • parent::
    :选中父级节点,
    /..
    也是选中父级,但是通常
    parent::
    用于写在
    []
    里面作为条件来判断
  • child::
    :选中子级节点,
    /
    也是选中子级,通常也是作为条件来使用
  • preceding-sibling::
    :选中同一层级的前面所有兄弟节点
  • following-sibling::
    :选中同一层级的后面所有兄弟节点
  • ancestor::
    :选中祖先节点,包括父级以及更上层的节点
  • descendant::
    :选中当前节点下面的所有节点,包括子级

举例:

//*[ancestor::div]

选中所有元素中,上级是 div 的元素,其实也就是选中了所有元素,来看看这个
//ancestor::div

只选中了一个元素。

两者的区别如下:
//*[ancestor::div]
选中的
*
表示所有元素,这些元素条件是
[ancestor::div]
父级及以上有 div。
//ancestor::div
选中的是作为别人父级及以上的 div,也就是选中的是 div,这个 div 的是别人的父级或者爷级等
两者是完全不同的概念

美团 APP 匹配示例

看了半天 HTML,我们来了解一下 APP 中的 XML,通常匹配 APP 比网页复杂太多,基本就那几个元素,而且属性名基本都一样,所以常用的手段还是使用各种条件来进行限制匹配,下面来看一个例子。

 <android.view.View index="5" class="android.view.View" text="" checked="false" clickable="true">
  <android.widget.TextView index="1" class="android.widget.TextView" text="象山酥院(湛江印象汇店)" checked="false"/>
  <android.widget.TextView index="2" class="android.widget.TextView" text="" checked="false" clickable="true"/>
  <android.view.View index="3" class="android.view.View" text="" checked="false">
    <android.widget.TextView index="0" class="android.widget.TextView" text="5.0" checked="false" />
  </android.view.View>
  <android.widget.TextView index="4" class="android.widget.TextView" text="周销量 872" checked="false" />
</android.view.View>
<android.view.View index="5" class="android.view.View" text="" checked="false" clickable="true">
  <android.widget.TextView index="1" class="android.widget.TextView" text="蜜雪冰城" checked="false"/>
  <android.widget.TextView index="2" class="android.widget.TextView" text="" checked="false" clickable="true"/>
  <android.view.View index="3" class="android.view.View" text="" checked="false">
    <android.widget.TextView index="0" class="android.widget.TextView" text="5.0" checked="false"/>
  </android.view.View>
  <android.widget.TextView index="4" class="android.widget.TextView" text="周销量 2322" checked="false"/>
</android.view.View>

上面代码为美团的城市列表页面的 UI XML 代码,其中每个元素都包含大量相同的属性和属性值,关键在于整个页面,任何地方基本就是
android.view.View

android.widget.TextView
,像匹配 HTML 那样元素显然行不通。

示例:获取两个商品的评分
//*[@text and ancestor::*/following-sibling::*[contains(@text, '周销量')]]
规则解释:获取任何包含 text 属性的元素,它的父级的的兄弟元素必须是一个 text 值中包含 "周销量"的元素。
我这里没有使用
[1][2][3]
来定位,是因为不同商品的属性很多时候不一样。

通常还是根据想要的元素的位置,以及相邻元素的特征来定位,首先找到独特的文本,比如上面的周销量是固定会出现的,还有
¥
符号也可以,这些都是位置和文本值固定的,找到这个的位置,再去定位需要的元素的位置。

工具推荐

  1. 谷歌浏览器-审查元素-
    ctrl + f
    ,可以直接输入 xpath 语句
  2. 谷歌浏览器-selectorshub 插件,文中使用的是这个插件

在尝试从一个使用Cloudflare Web应用程序防火墙(WAF)保护的网站获取数据时,我遇到了一些挑战.该网站的安全措施非常严格,以至于在正常浏览几个页面后,Cloudflare的检查页面就会出现.

传统的HTTP客户端方法,如直接使用httpclient来抓取页面数据,很快就会遭遇阻碍.

即便尝试使用代理IP池,问题依旧存在,因为Cloudflare的检测机制能够在短时间内多次访问后迅速触发.在多次尝试后,我决定使用playwright这个自动化库来模拟正常的浏览器行为.

虽然在使用playwright的过程中遇到了一些问题,但我最终找到了解决方案.现在,尽管速度稍慢,但我能够正常地从网站获取数据.接下来,我将分享如何克服这些挑战的经验.

直接403

这是cloudflare的基本防护,他会检查是否使用了webdriver进行模拟爬取.
解决方式:

...
final BrowserContext context = browser.newContext();
...
context.setDefaultTimeout(180_000);
context.setDefaultNavigationTimeout(180_000);
context.addInitScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})");

这里同时也将默认超时拉长,避免在等待某些元素触发的条件下超时.

使用本地安装的浏览器

可以直接指定本地安装的浏览器:

While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers.

Available channels are chrome, msedge, chrome-beta, msedge-beta or msedge-dev.

启动的时候指定即可:

Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("msedge"));

是否触发了cloudflare判断

因为触发了cloudflare页面的url并不会有明显的变化,只有多一次跳转.
这里直接使用页面元素即可, 处理代码如下:

    private static void checkMeetChallenge(Page page) throws Exception {
        ElementHandle stage = page.querySelector("div#challenge-stage");
        int checkCount = 1;
        while (stage != null) {
            tryToClickChallenge(page, stage);
            if (checkCount >= 6) {
                if (page.querySelector("div#challenge-stage") != null) {
                    throw new Exception("meet challenge restart");
                }
            }
            log.info("handleCategory - meet challenge. wait 20s to check it again. count:{}", checkCount);
            checkCount++;
            page.waitForTimeout(20_000);
            stage = page.querySelector("div#challenge-stage");
        }
    }

在上层的调用代码中,我先判断了我需要的元素是否存在,如果不存在有两种可能,一种是已经爬完了,另一种是触发了cloudflare.
那个meet challenge restart是一层兜底, 如果点击失败加上长时间等待还在, 那就重启浏览器, 重启之后大概率不会重新触发防护, 也算是最后一个措施了.

处理防护页的人工点击

首先先看一下那个防护页面的结构:

<div id="challenge-stage" style="display: flex;">
    <div id="turnstile-wrapper" class="captcha-prompt spacer">
        <div><iframe
                src="https://challenges.cloudflare.com/cdn-cgi/challenge-platform/xxxxxx"
                allow="cross-origin-isolated; fullscreen"
                sandbox="allow-same-origin allow-scripts allow-popups" id="cf-chl-widget-sppzq" tabindex="0"
                title="包含  Cloudflare  安全质询的小组件 "
                style="border: none; overflow: hidden; width: 300px; height: 65px;"></iframe><input
                type="hidden" name="cf-turnstile-response" id="cf-chl-widget-sppzq_response"></div>
    </div>
</div>

点击的组件被放在了一个iframe里,正常要获取元素点击比较麻烦.
但是
playwright
直接模拟鼠标在位置上点击即可.
具体思路是定位到外层的
div#challenge-stage
, 然后用
page.mouse().move
移动到元素上,在用click()点击.
代码:

BoundingBox box = stageDiv.boundingBox();
page.mouse().move(box.x + 100, box.y + box.height / 2);
page.waitForTimeout(1_000 + ThreadLocalRandom.current().nextInt(100, 1000));
page.mouse().click(box.x + 100, box.y + box.height / 2);

其实你不点击它也没关系,我在测试的时候发现这个challenge页面会在1分钟左右自己消失,可以在检测出是challenge之后循环waitForTimeout即可.

最后就是要注意时间间隔,中间留一点时间.
这些做完基本就可以慢慢获取数据了.

我之前还做了很多随机滚动(mouse.wheel),移动到下一页点击等措施,结果发现做不做一样,可能在一些情况下比较有用.
因为
playwright
很多动作其实是要scheduled的,它并不立即触发,比如你click下一页之后就算
waitForLoadState
再用
locator

querySelector
获取元素他还是会报错,这种情况可以手动
waitForTimeout
一下,我不知道这样处理是不是常规做法,但至少解决了我的问题.

相关参考链接:
playwright
playwright Browsers
How to Bypass Cloudflare with Playwright in 2024
original blog