2024年3月

在过去的实践中,我们通常通过爬取HTML网页来解析并提取所需数据,然而这只是一种方法。另一种更为直接的方式是通过发送HTTP请求来获取数据。考虑到大多数常见服务商的数据都是通过HTTP接口封装的,因此我们今天的讨论主题是如何通过调用接口来获取所需数据。

目前来看,大多数的http接口数据都采用restful风格,通常使用JSON格式来发送和接收数据。对于那些对此不太了解的零基础学者,建议先学习相关知识点。在本章学习过程中,我们将主要以腾讯云开发者社区作为主要平台,练习爬取接口数据。

接口爬取

接口爬取并不复杂,首先需要在浏览器中打开腾讯云社区的网页,然后按下F12打开控制台,接着浏览控制台中的请求数据接口,有些接口可能一眼难以识别,但通常可以跳过细致查看,因为在开发过程中,最关键的是能从名称中直观理解其作用,大型公司通常设计得相当清晰。我们首先尝试爬取主页的活动数据。

image

我们可以选择使用XHR来单独查看请求,这样就能排除掉页面、js、css等无关的请求,逐个检查接口,找到我们需要的内容。这个特定接口就是我们必须记住的,其他的都是多余的。

便利工具

在这里,我们想向大家介绍一个非常实用的开发爬虫工具,它就是https://curlconverter.com/

我是通过偶然的机会发现了这个工具的,它的确大大提升了我的爬虫效率。通常情况下,当我们找到了需要爬取的接口时,我们需要编写Python代码来发起请求,可能还要处理各种请求头和cookie,这一过程会消耗大量时间。而这个工具则帮助我们省去了这些繁琐的步骤,使得整个过程变得更加高效。

首先,我们在后台查找到目标请求,然后通过右键点击复制该请求。以Edge浏览器为例,具体操作如下所示:

image

在将内容复制后,我们可以直接前往这个在线工具网站,将其粘贴进去,从而生成相应的Python代码。这里以使用requests库为例进行演示。当你浏览该网站时,你可以选择你喜欢的任何编程语言进行相应代码的生成。

image

我们只需简单地将其复制粘贴到IDE中,然后便可直接运行代码。

社区首页

一旦我们掌握了这种方法,基本上就可以获取想要爬取的所有数据,只要避免频繁请求而被识别为机器人爬虫。让我们首先尝试爬取社区首页的文章,以了解今年哪些类别的文章备受关注。以下是示例代码:

import datetime
import requests

ad_list = []
article_list = []
article_total = 0
def get_article_list(pageNumber):
    global article_total,article_list
    ## 这里不需要cookie也是可以的。
    headers = {
        'authority': 'cloud.tencent.com',
        'accept': 'application/json, text/plain, */*',
        'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'content-type': 'application/json',
        'cookie': 'qcloud_uid=3db7bb7a1663470df3290f692c4a7206; language=zh; lastLoginIdentity=e382a0dd45ecf7f063e05751e7321e14; _ga_6WSZ0YS5ZQ=GS1.1.1685003902.1.1.1685004114.0.0.0; loginType=email; _ga_7PG2H0XLX2=GS1.2.1705284469.2.1.1705284470.59.0.0; lastLoginType=email; _gcl_au=1.1.315225951.1705902067; _ga_95GG3X2GMT=GS1.1.1707206895.14.0.1707212112.0.0.0; _ga=GA1.2.100014169188; mfaRMId=0092627a989e3ef79957c2257ea910f8; qcloud_from=qcloud.google.seo-1709083904498; qcstats_seo_keywords=%E5%93%81%E7%89%8C%E8%AF%8D-%E5%93%81%E7%89%8C%E8%AF%8D-%E8%85%BE%E8%AE%AF%E4%BA%91; from_column=20421; cpskey=1f39dac98ac4cc96c6503bdb4f49994f; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22100014169188%22%2C%22first_id%22%3A%221878e0e485666666b-0be585a75d9ef-7e57547d-2073600-1878e0e4852ec0%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_utm_medium%22%3A%22ocpc%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTg3OGUwZTQ4NTExMWItMGJlNTg1YTc1ZDllZi03ZTU3NTQ3ZC0yMDczNjAwLTE4NzhlMGU0ODUyZWMwIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiMTAwMDE0MTY5MTg4In0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%22100014169188%22%7D%2C%22%24device_id%22%3A%221878e0e485666666b-0be585a75d9ef-7e57547d-2073600-1878e0e4852ec0%22%7D; qcmainCSRFToken=NSsz_8Bfx1S_; qcloud_visitId=3e799aa8be55222ade40e7ab9b8be875; intl=; _gat=1; trafficParams=***%24%3Btimestamp%3D1710467373372%3Bfrom_type%3Dserver%3Btrack%3Da7699f0f-3309-4c6b-9740-475f6c5f11ba%3B%24***',
        'origin': 'https://cloud.tencent.com',
        'referer': 'https://cloud.tencent.com/developer',
        'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
    }

    json_data = {
        'pageNumber': pageNumber,
        'pageSize': 100,
        'type': 'recommend', ## 文章是否被推荐到首页
    }

    response = requests.post(
        'https://cloud.tencent.com/developer/api/home/article-list',
        headers=headers,
        json=json_data,
    )
    news_list = response.json()
    for article in news_list['list']:
        ## 处理一下文章的类别
        handle_tag(article)
        ## 可以自己解析首页的文章,只拿你想要的
        article_list.append({
            "article_title": article['title'],
            "article_date": article['createTime'],
            "article_summary": article['summary']
        })
    article_total = news_list['total']
    fixed_time = datetime.datetime(2023, 1, 1)
    timestamp = int(fixed_time.timestamp())
    print(f'{article_list[-1]["article_date"]}和{timestamp}')
    ## 判断一下是否已经是最后一页
    return 0 if article_list[-1]['article_date'] < timestamp else 1

def handle_tag(article):
    # 遍历解析后的数据,统计每个tagName的数据量
    for item in article['tags']:
        tag_name = item["tagName"]
        if tag_name in tag_counts:
            tag_counts[tag_name] += 1
        else:
            tag_counts[tag_name] = 1

def get_top_10():
    # 根据数据量对tagName进行排序
    sorted_tag_counts = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)

    # 取前10个tagName
    top_10_tags = sorted_tag_counts[:10]

    # 打印前10个tagName的数据量统计
    for tag, count in top_10_tags:
        print(f"{tag}: {count}")

page_num = 1
while True:
    num = get_article_list(page_num)
    page_num = page_num + 1
    if num == 0:
        break
    
get_top_10()

代码首先通过API获取文章列表数据,然后遍历每篇文章的标签信息,统计每个标签出现的次数,最后输出每个标签和其对应的数据量。这样可以帮助用户了解哪些标签在文章中出现频率较高。除了这些,我还额外处理轮播活动的数据,获取更全面的活动信息。

import datetime
import requests

ad_list = []
def get_ads():
    global ad_list
    headers = {
        'authority': 'cloud.tencent.com',
        'accept': 'application/json, text/plain, */*',
        'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'content-type': 'application/json',
        'origin': 'https://cloud.tencent.com',
        'referer': 'https://cloud.tencent.com/developer',
        'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
    }

    json_data = {
        'cate': 'cloud_banner',
        'preview': False
    }

    response = requests.post('https://cloud.tencent.com/developer/api/common/getAds', headers=headers, json=json_data)
    news_list = response.json()
    ad_list = [{'pcTitle': item['content']['pcTitle'], 'url': item['content']['url']} for item in news_list['list']]
get_ads()
print(ad_list)

我的文章

如果我们希望对我们自己的文章进行分析和处理,首先需要进行登录。原本我打算尝试通过编写代码实现免登录,但是仔细研究后台 JavaScript 和登录验证后发现实现起来涉及的内容过多,对我们这样以学习为主的学者来说并不适合。

确保我已经登录的标识是通过 cookie 实现的。Cookie 在这里的作用是保持用户登录状态,使用户在不同页面之间保持登录状态。由于 HTTP 是无状态的,需要一种方法来保持会话连接,而这种方法就是使用 Cookie。对于请求来说,Cookie 就是一串字符串,服务器会自动解析它,无需我们手动管理。因此,我只需在网页登录后使用工具复制粘贴 Cookie 即可。尽管我花费了一整天,但仍未成功编写代码实现登录并获取 Cookie。因此,我们最好选择最简单的方法。

示例代码如下:

import requests
def get_my_article(page_num):
    headers = {
        'authority': 'cloud.tencent.com',
        'accept': 'application/json, text/plain, */*',
        'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'content-type': 'application/json',
        'cookie': '',# 这里需要复粘贴你自己的cookie。
        'origin': 'https://cloud.tencent.com',
        'referer': 'https://cloud.tencent.com/developer/creator/article',
        'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
    }
    page_size = 20
    json_data = {
        'hostStatus': 0,
        'sortType': 'create',
        'page': page_num,
        'pagesize': page_size,
    }

    response = requests.post(
        'https://cloud.tencent.com/developer/api/creator/articleList',
        headers=headers,
        json=json_data,
    )
    news_list = response.json()
    for article in news_list['list']:
        # handle_tag(article)
        # 这里我就不解析了,简单打印一下吧。
        my_article_list.append({
            "article_title": article['title'],
            "article_date": article['createTime'],
            "article_summary": article['summary']
        })
    article_total = news_list['total']
    if page_num*page_size > article_total:
        return 0
    else:
        return 1

在这个函数中,参数page_num代表着要获取的文章列表页数。请务必留意,请求头中的headers需要包含用户自行提供的Cookie信息,这样才能确保程序正常运行。您可以在这里获取到Cookie信息,只需将其复制粘贴即可。详见下图:

image

总结

在过去的实践中,我们常常通过爬取HTML网页来解析和提取数据,因此今天我们讨论了如何通过调用接口来获取所需数据。本文通过示例代码展示了如何爬取社区首页的文章和活动数据,以及如何爬取自己的文章列表。通过这些实践,我们可以更好地理解和运用接口爬取技术。

本文收录于
Github.com/niumoo/JavaNotes
,Java 系列文档,数据结构与算法!
本文收录于网站:
https://www.wdbyte.com/
,我的公众号:
程序猿阿朗

JSON Web Token

JWT
)是一种可以在多方之间安全共享数据的开放标准,JWT 数据经过编码和数字签名生成,可以确保其真实性,也因此 JWT 通常用于身份认证。这篇文章会介绍什么是 JWT,JWT 的应用场景以及组成结构,最后分析它的优点及局限性。

传统认证方式的问题

在介绍 JWT 之前,先看下传统认证方式有什么问题,这里的传统认证方式是指
Session-Cookie
方式。有小伙伴可能要说了,传统认证方式能有什么问题,如果有问题肯定是你代码写的不好。其实不是,有些问题存在于方案本身。

先来看一下传统的认证方式流程:

  1. 客户端输入用户名密码,点击登录。
  2. 服务端验证用户名密码,校验通过,服务端存储 Session 数据,如身份,权限。
  3. 服务端响应 Cookie,一般内容是一个 Session ID,客户端收到 Cookie 后存储。
  4. 客户端后续请求携带 Cookie 作为身份认证凭据,服务端验证 Cookie 得知用户身份。

这是常见的认证流程,但是这种认证方式存在下面几个问题。

状态存储负担

Session-Cookie
方式因为服务端要存储当前会话信息,而且必不可少, 这就额外增加了存储负担,而且在分布式系统中,还要考虑不同机器之间的会话状态同步问题。有时还需要部署独立的认证服务。不易维护。

跨域问题

基于 Cookie 会话的认证方式,在进行跨域请求时存在难点,Cookie 不会跟随跨域请求。认证信息带不过去,当然,聪明的小伙伴可以通过设置客户端参数,配置服务端参数等操作来允许跨域,不过有点麻烦了。

CSRF 攻击风险

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的 Web 安全漏洞。有些小伙伴不知道是什么意思,看下面流程:

  1. 你登录了 QQ 空间,传统认证方式,客户端成功存储了 Cookie。
  2. 你的 “好友X” 给你发了一个链接,标题 “专家:吃淀粉肠活不过三年”。
  3. 突然你感觉仿佛昨晚淀粉肠的香味还没散去,你迫不及待的点开了,没想到自动跳转到了 QQ 空间,你感觉莫名其妙,大失所望,立马关掉。
  4. 但是没想到的是这个跳转请求了空间说说发表接口,因为你之前登录过,Cookie 状态还在。说说直接发表成功了。那马上可能就有好友问你空间发的乱七八糟的内容是什么意思了。

QQ 空间曾经也确实出现过 CSRF 漏洞。

在解决这几个问题上,JWT 具有天然优势,它存储在客户端,服务端无状态。Token 可以不存在 Cookie 中,轻松跨域又减少了 CSRF 风险。

JWT 是什么

JWT(JSON Web Tokens)它定义了一种
紧凑

自包含
的方式用于在各方之间作为 JSON 对象安全地传递信息。紧凑意味着内容尽可能的短小。自包含意味着内容中包含了身份信息。这个信息可以用于
身份认证
,也可以用于
信息交换
。由于信息会使用
密钥进行数字签名
,因此JWT 可以
被验证
以及信任。

JWT 应用场景

常见的 JWT 应用常见有 JWT 授权和信息交换:

  • 授权
    :JWT 被应用最多的场景,用户登录后服务端响应一个 JWT,后续的请求都携带 JWT内容,以此验证用户身份。使用 JWT 可以进行单点登录,可以跨域。
  • 信息交换
    :因为 JWT 需要使用密钥进行签名,因此使用 JWT 安全的传输信息也是一个好方法,签名可以确保消息发送人没有问题,确保消息没有被篡改。

JWT 组成结构

JWT 由
小数点
分割的三部分组成,如
xxxxx.yyyyy.zzzzz
,这三部分对应的是的
标头(Header)、负载(Payload)、签名(Signature)
,每部分使用
Base64Url
进行编码,

下面是一个真实 JWT 示例:

注意:JWT 为紧凑形式,没有换行,这里为了方便阅读,进行了换行。

标头 Header

Header 部分 Base64Url 解码后可以看到两个字段,
alg
指定签名算法,
typ
指定 Token 类型。

{
  "alg": "HS256",
  "typ": "JWT"
}

对上述标头对象进行 Base64Url 编码以形成 JWT 的第一部分。

负载 Payload

第二部分中存放了实际需要的数据,用户可以自定义内容,如用户身份信息。

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同时 JWT 也规定了几个官方字段:

iss (issuer):签发人
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
exp (expiration time):过期时间
iat (Issued At):签发时间
jti (JWT ID):编号

可以看到不管第一部分还是第二部分,字段名都是三位字母,这也是为了内容的紧凑。对 JWT 负载进行 Base64Url 编码以形成 JWT 的第二部分。

特别注意:由于只要 Base64Url 解码就可以看到第二部分内容,因此不能在 Payload 中存储敏感信息。

签名 Signature

签名 Signature 的生成依赖
标头 Header

负载 Payload
,同时要有拥有用于签名的
密钥
,因此签名可以用于验证 JWT 的发送者是否正确,并确保消息没有被篡改。

Signature = HMACSHA256(
  base64UrlEncode(header) + '.' +
  base64UrlEncode(payload),
  secret)

JWT 官网也提供
JWT 在线解码验证工具
,可以访问查看。

JWT 身份认证

使用 JWT 进行身份认证的工作流程如下:

  1. 用户使用登录凭证(如用户名和密码)进行登录。

  2. 服务器验证凭证的正确性,并创建一个包含用户信息的 JWT。

  3. 服务器对 JWT 进行签名,然后将其发送回用户。

  4. 用户将 JWT 存储在客户端(如 localStorage),并在随后的请求中随同发送。

    如添加到请求头:
    Authorization: Bearer <token>

  5. 服务器在接收到请求后,验证 JWT 的签名并解析其内容,确认用户的身份,然后返回请求的数据。

  6. JWT 可能在一定时间后过期,用户需要重新登录获取新的 JWT。

JWT 的特点

JWT 有下面几个特点。

  1. 紧凑
    :JWT 设计十分紧凑,结果较小,可以在参数,请求头中传输。
  2. 自包含
    :JWT 自身包含了用户验证的所需信息,避免了多次查询数据库。
  3. 跨语言
    :JWT 使用 JSON 格式,现代编程语言都有对 JSON 的支持。
  4. 安全性
    :JWT 需要使用密钥进行数据签名,密钥不泄露,JWT 就是安全的。但是因为 JWT 自包含和 Base64Url 编码特性,JWT 中的信息可以被直接读取,因此建议使用 HTTPS 协议。如果对安全性要求较高,
    还可以对 JWT 内容在进行一次加密
    (如 AES)。
  5. 分布式环境友好
    :因为 JWT 在服务端无状态,因此 JWT 适用于单点登录,同时可以
    跨域
  6. 不可撤销
    :一旦 JWT 签发了,在有效期内将会一直有效,除非服务器增加额外逻辑来强制撤销某个 JWT Token,如黑名单机制。
  7. 性能问题
    :虽然避免了查询数据库,但是服务器仍需对每个请求中的 JWT 进行解码和验证,如果请求量巨大,这也可能成为性能瓶颈。

JWT 最佳实践

JWT 存在优点,也有很多风险与挑战,参考前人的最佳实践可以少走弯路。

  1. 内容紧凑最小化:最小限度的减少 JWT 负载中的内容,避免存储敏感数据,只存储重要数据。某些服务器不接受大于 8KB 的请求头。
  2. 验证必不可少:每次收到 JWT 都要进行验证,内容验证,过期时间验证。发行者验证。
  3. 使用 JWT 库:不要自己编写 JWT 类库,密码学和安全都是非常复杂的东西,使用专业的类库好过自己编写。
  4. JWT 过期时间:设计合理的过期时间,因为 JWT 一旦颁发,无法删除。过长的有效期存在风险。

总体而言,JWT 提供了一种相对简单且有效的方式来处理身份验证问题,但是需要注意JWT 安全性和细节问题,以确保 JWT 可以在应用中正确且安全地使用。

预告:下一篇文章会介绍如何在 Java 中使用 JWT 进行身份验证。

参考

预告下一篇:Java 中如何使用 JWT 进行身份认证

本文收录于
Github.com/niumoo/JavaNotes
,Java 系列文档,数据结构与算法!
本文收录于网站:
https://www.wdbyte.com/
,我的公众号:
程序猿阿朗

拦截器Interceptors是一种可以在编译时以声明方式替换原有应用的方法。

这种替换是通过让Interceptors声明它拦截的调用的源位置来实现的。

您可以使用拦截器作为源生成器的一部分进行修改,而不是向现有源编译添加代码。

演示

使用 .NET 8 创建一个控制台应用程序。并在PropertyGroup中添加以下配置.。需要将其中WebApplication6替换为自己的命名空间。

<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);WebApplication6</InterceptorsPreviewNamespaces>

然后在单独的文件中创建InterceptsLocationAttribute。其命名空间必须是System.Runtime.CompilerServices,而不是应用程序的命名空间。

namespaceSystem.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple
= true)]public sealed class InterceptsLocationAttribute(string filePath, int line, intcharacter) : Attribute
{
}
}

该属性包含三个参数。

  • filePath是您要拦截的文件的路径。
  • line是您要拦截的代码行。
  • character是您要拦截的代码字符位置。

接着来创建一个具有三种方法的类,模拟新增/查询用户作为示例:

public classGetUserService
{
//This method will not be intercepted; public voidGetUserName()
{
Console.WriteLine(
"GetUserName");
}
//This method will be intercepted; public voidAddUser()
{
Console.WriteLine(
"AddUser");
}
//This method will not be intercepted; public voidDeleteUser()
{
Console.WriteLine(
"DeleteUser");
}
}

在 Program.cs 文件中,我创建了此类的一个实例,并创建了对这三个方法中每一个的调用。输出如下所示:

var userService = newGetUserService();

userService.GetUserName();
userService.AddUser();
userService.DeleteUser();

现在让我们创建拦截类。该类必须遵循以下规则:

  • 一定是一个static类。
  • 必须是我们要拦截的类的扩展方法。
  • 必须具有该InterceptsLocation属性,其中包含我们要拦截的文件路径的值以及行号和字符号。

usingSystem.Runtime.CompilerServices;namespaceWebApplication6
{
public static classInterceptUserService
{
[InterceptsLocation(
filePath:
@"D:\demo\test\ConsoleApp1\WebApplication6\Program.cs",
line:
14,
character:
25)]public static void InterceptMethodAddUser(thisGetUserService example)
{
Console.WriteLine(
"Interceptor is here!");
}
}
}

在此示例中,将拦截AddUser方法,并且将执行InterceptMethodAddUser方法,而不是执行方法AddUser。

filePath可以按以下方式获取

行号和字符号可以按以下方式获取

现在运行代码,方法AddUser将被拦截,并且不会被执行,而是实际执行拦截器方法,以下是输出:

一:背景

1. 讲故事

前几天有位朋友在微信上找到我,说他的软件卡死了,分析了下也不知道是咋回事,让我帮忙看一下,很多朋友都知道,我分析dump是免费的,当然也不是所有的dump我都能搞定,也只能尽自己最大能力帮助别人缩小问题范围吧,既然dump有了,接下来就开启分析之路。

二:WinDbg分析

1. 为什么会卡死

不同类型的程序卡死的解决思路是不一样的,朋友也说了是窗体程序,那就重点观察下主线程吧,使用 k 命令即可。


0:000> k 25
 # Child-SP          RetAddr               Call Site
00 00000000`007fc8d8 00007ffd`87439b13     ntdll!NtWaitForAlertByThreadId+0x14
01 00000000`007fc8e0 00007ffd`87439a06     ntdll!RtlpWaitOnAddressWithTimeout+0x43
02 00000000`007fc910 00007ffd`8743987d     ntdll!RtlpWaitOnAddress+0xae
03 00000000`007fc980 00007ffd`87435fdc     ntdll!RtlpWaitOnCriticalSection+0xd9
04 00000000`007fc9f0 00007ffd`87435ef0     ntdll!RtlpEnterCriticalSectionContended+0xdc
05 00000000`007fca20 00007ffd`536839ea     ntdll!RtlEnterCriticalSection+0x40
06 00000000`007fca50 00007ffd`5368470a     AcLayers!NS_VirtualRegistry::CRegLock::CRegLock+0x1a
07 00000000`007fca90 00007ffd`536726d2     AcLayers!NS_VirtualRegistry::APIHook_RegOpenKeyExW+0x2a
08 00000000`007fcb10 00007ffd`778e550b     AcLayers!NS_WRPMitigation::APIHook_RegOpenKeyExW+0x42
09 00000000`007fcb60 00007ffd`778e5437     xxx!GetCodePageForFont+0xa7
0a 00000000`007fcc90 00007ffd`778e5296     xxx!CToolTipsMgr::NewFont+0x113
0b 00000000`007fcda0 00007ffd`778e18f9     xxx!CToolTipsMgr::LoadTheme+0xb2
0c 00000000`007fcdd0 00007ffd`84b9ca66     xxx!CToolTipsMgr::s_ToolTipsWndProc+0x1b9
0d 00000000`007fce10 00007ffd`84b9c34b     user32!UserCallWinProcCheckWow+0x266
0e 00000000`007fcf90 00007ffd`4f36b1cc     user32!CallWindowProcW+0x8b
0f 00000000`007fcfe0 00007ffd`4f39ccac     System_Windows_Forms_ni!System.Windows.Forms.NativeWindow.DefWndProc+0x9c
10 00000000`007fd090 00007ffd`4f39cc05     System_Windows_Forms_ni!System.Windows.Forms.ToolTip.WndProc+0x9c
11 00000000`007fd260 00007ffd`4f36a3a3     System_Windows_Forms_ni!System.Windows.Forms.ToolTip.ToolTipNativeWindow.WndProc+0x15
12 00000000`007fd290 00007ffd`4f9e1161     System_Windows_Forms_ni!System.Windows.Forms.NativeWindow.Callback+0xc3
13 00000000`007fd330 00007ffd`52c8222e     System_Windows_Forms_ni+0x8d1161
14 00000000`007fd3a0 00007ffd`84b9ca66     clr!UMThunkStub+0x6e
15 00000000`007fd430 00007ffd`84b9c78c     user32!UserCallWinProcCheckWow+0x266
16 00000000`007fd5b0 00007ffd`84bb3b32     user32!DispatchClientMessage+0x9c
17 00000000`007fd610 00007ffd`874c22c4     user32!__fnINLPCREATESTRUCT+0xa2
18 00000000`007fd670 00007ffd`836a1f24     ntdll!KiUserCallbackDispatcherContinue
19 00000000`007fd7e8 00007ffd`84ba15df     win32u!NtUserCreateWindowEx+0x14
1a 00000000`007fd7f0 00007ffd`84ba11d4     user32!VerNtUserCreateWindowEx+0x20f
1b 00000000`007fdb80 00007ffd`84ba1012     user32!CreateWindowInternal+0x1b4
1c 00000000`007fdce0 00007ffd`4f3e8098     user32!CreateWindowExW+0x82
1d 00000000`007fdd70 00007ffd`4f3696f0     System_Windows_Forms_ni+0x2d8098
...

从卦象看,很明显主线程卡在 NtWaitForAlertByThreadId 上,这是有问题的,接下来我们仔细解读下线程栈。

  • DispatchClientMessage

这个方法表示当前从 queue 中拿到了别的线程通过 Invoke 送过来的信息,正在处理中。

  • LoadTheme

这个方法表示正在用主线程更新窗体样式

  • APIHook_RegOpenKeyExW

首先说一下 AcLayers.dll,专业名词叫
垫片
,详情可以看一下《软件调试》,它主要用来处理一些系统级兼容性的问题,然后可以看到它在查询注册表时有一个lock操作。

在非托管代码中,lock 一般都用 临界区(CriticalSection) 实现,那到底它等待的临界区被谁持有着呢?

2. 谁持有着临界区锁

要想获取锁的持有信息,可以使用
!cs -l
或者
!locks
,但这里要提醒一下,在真实的dump分析过程中,有时候不准,所以更好的办法就是从线程栈上提取,那怎么提取呢? 其实就是寻找
ntdll!RtlEnterCriticalSection
方法的第一个参数即可,方法签名如下:


VOID RtlEnterCriticalSection(
  PRTL_CRITICAL_SECTION CriticalSection
);

接下来反汇编下
00007ffd536839ea
处的代码,看看 rcx 寄存器是怎么传下来的。


0:000> ub 00007ffd`536839ea
AcLayers!NS_VirtualRegistry::OPENKEY::AddEnumEntries<NS_VirtualRegistry::VIRTUALVAL>+0x11a:
00007ffd`536839ce cc              int     3
00007ffd`536839cf cc              int     3
AcLayers!NS_VirtualRegistry::CRegLock::CRegLock:
00007ffd`536839d0 48895c2408      mov     qword ptr [rsp+8],rbx
00007ffd`536839d5 57              push    rdi
00007ffd`536839d6 4883ec30        sub     rsp,30h
00007ffd`536839da 488bf9          mov     rdi,rcx
00007ffd`536839dd 488d0d4c7f0300  lea     rcx,[AcLayers!NS_VirtualRegistry::csRegCriticalSection (00007ffd`536bb930)]
00007ffd`536839e4 ff15ae660100    call    qword ptr [AcLayers!_imp_EnterCriticalSection (00007ffd`5369a098)]

从卦象上看,很吉利,这个 rcx 原来是一个全局变量
AcLayers!NS_VirtualRegistry::csRegCriticalSection
, 接下来用 !cs 观察下到底被谁持有着。


0:000> !cs AcLayers!NS_VirtualRegistry::csRegCriticalSection
-----------------------------------------
Critical section   = 0x00007ffd536bb930 (AcLayers!NS_VirtualRegistry::csRegCriticalSection+0x0)
DebugInfo          = 0x000000001c4e58e0
LOCKED
LockCount          = 0x2
WaiterWoken        = No
OwningThread       = 0x0000000000001d20
RecursionCount     = 0x1
LockSemaphore      = 0xFFFFFFFF
SpinCount          = 0x00000000020007ce

这又是一副吉卦,可以看到当前持有线程是
1d20
,那这个线程正在做什么呢?

3. 1d20 线程为什么持锁不释放

案情往前推进了一步,我们切过去观察下这个线程栈。


0:000> ~~[1d20]s
ntdll!NtDelayExecution+0x14:
00007ffd`874bec14 c3              ret
0:028> kL
 # Child-SP          RetAddr               Call Site
00 00000000`33ccd948 00007ffd`83955381     ntdll!NtDelayExecution+0x14
01 00000000`33ccd950 00007ffd`6d4a2361     KERNELBASE!SleepEx+0xa1
02 00000000`33ccd9f0 00007ffd`8520a75c     perfts!CloseLagPerfData+0x21
03 00000000`33ccda30 00007ffd`85209ccd     advapi32!CloseExtObjectLibrary+0xec
04 00000000`33ccda90 00007ffd`8396dc6a     advapi32!PerfRegCloseKey+0x15d
05 00000000`33ccdae0 00007ffd`839715e6     KERNELBASE!BaseRegCloseKeyInternal+0x72
06 00000000`33ccdb10 00007ffd`83935209     KERNELBASE!ClosePredefinedHandle+0x96
07 00000000`33ccdb40 00007ffd`53685d71     KERNELBASE!RegCloseKey+0x149
08 00000000`33ccdba0 00007ffd`53683ae5     AcLayers!NS_VirtualRegistry::CVirtualRegistry::CloseKey+0xbd
09 00000000`33ccdbf0 00007ffd`51c7737e     AcLayers!NS_VirtualRegistry::APIHook_RegCloseKey+0x25
0a 00000000`33ccdc30 00007ffd`51bf4be2     mscorlib_ni+0x58737e
0b 00000000`33ccdce0 00007ffd`513c356a     mscorlib_ni!Microsoft.Win32.RegistryKey.Dispose+0x72
0c 00000000`33ccdd20 00007ffd`513c34b9     System_ni!System.Diagnostics.PerformanceCounterLib.GetStringTable+0x41a
...
13 00000000`33cce050 00007ffd`513bfe3c     System_ni!System.Diagnostics.PerformanceCounter..ctor+0xd7
14 00000000`33cce0a0 00007ffc`f45cb2ce     System_ni!System.Diagnostics.PerformanceCounter..ctor+0x1c
15 00000000`33cce0d0 00007ffc`f45cb14c     0x00007ffc`f45cb2ce
16 00000000`33cce120 00007ffc`f45cb023     0x00007ffc`f45cb14c
...

从卦中看,这个线程貌似在用
CloseLagPerfData
方法关闭一些东西时一直在Sleep等待,可以反汇编
00007ffd6d4a2361
处代码看看等待多久。


0:028> ub 00007ffd`6d4a2361
perfts!CloseLagPerfData+0x5:
00007ffd`6d4a2345 55              push    rbp
00007ffd`6d4a2346 488bec          mov     rbp,rsp
00007ffd`6d4a2349 4883ec30        sub     rsp,30h
00007ffd`6d4a234d e8720e0000      call    perfts!LagCounterManager::Cleanup (00007ffd`6d4a31c4)
00007ffd`6d4a2352 33db            xor     ebx,ebx
00007ffd`6d4a2354 eb0b            jmp     perfts!CloseLagPerfData+0x21 (00007ffd`6d4a2361)
00007ffd`6d4a2356 b964000000      mov     ecx,64h
00007ffd`6d4a235b ff15c74e0000    call    qword ptr [perfts!_imp_Sleep (00007ffd`6d4a7228)]
...

从卦中的
mov ecx,64h
可以看到是 Sleep(100) 毫秒,更多细节也没空继续追究了,但不管怎么样,它是由上层的计数器类
PerformanceCounter
引发的,这里学一下 4S 店的做法,让朋友能不能不要调用
PerformanceCounter
这个类,咱躲开他就可以了,截图如下:

去掉之后,朋友反馈问题消失。

三:总结

说来也奇怪,最近发现了二起由
PerformanceCounter
引发的程序卡死,把经验留在这里,希望后来人少踩坑吧!

图片名称

Garnet 是 Microsoft Research 的远程缓存存储,提供强大的性能(吞吐量和延迟)、可扩展性、存储、恢复、集群分片、密钥迁移和复制功能。

Garnet 可以与现有的 Redis 客户端配合使用。

Garnet 是 Microsoft Research 推出的一种新型远程缓存存储,具有多种独特优势:

  • Garnet 采用流行的 RESP 线路协议作为起点,这使得可以从当今大多数编程语言(例如 C# 中的 StackExchange.Redis)中可用的未经修改的 Redis 客户端使用 Garnet。
  • 相对于同类开源缓存存储,Garnet 通过许多客户端连接和小批量提供更好的吞吐量和可扩展性,从而节省大型应用程序和服务的成本。
  • Garnet 使用启用了加速 TCP 的商品云 (Azure) VM 展示了极低的客户端延迟(在 99.9% 时通常低于 300 微秒),这对于现实场景至关重要。
  • Garnet 基于最新的 .NET 技术,具有跨平台、可扩展和现代化的特点。它被设计为易于开发和发展,而不会牺牲常见情况下的性能。我们利用 .NET 丰富的库生态系统来扩展 API,并提供开放的优化机会。由于我们对 .NET 的精心使用,Garnet 在 Linux 和 Windows 上都实现了最先进的性能。

该存储库包含构建和运行 Garnet 的代码。有关更多信息和文档,请访问网站:https://microsoft.github.io/garnet。

特性总结

Garnet 实现了广泛的 API,包括原始字符串(例如,获取、设置和密钥过期)、分析(例如,HyperLogLog 和 Bitmap)和对象(例如,排序集和列表)操作。它可以以客户端 RESP 事务和我们自己的 C# 服务器端存储过程的形式处理多密钥事务,并允许用户在原始字符串和新对象类型上定义自定义操作,所有这些都以 C# 的便利性和安全性实现,导致开发自定义扩展的门槛较低。

Garnet 使用快速且可插拔的网络层,支持未来的扩展,例如利用内核旁路堆栈。它使用强大的 .NET SslStream 库支持安全传输层安全 (TLS) 通信以及基本访问控制。 Garnet 的存储层称为 Tsavorite,是从我们之前的开源项目 FASTER 中分叉出来的,包括强大的数据库功能,例如线程可扩展性、分层存储支持(内存、SSD 和云存储)、快速非阻塞检查点、恢复、操作日志记录以实现持久性、多键事务支持以及更好的内存管理和重用。最后,Garnet 支持集群操作模式,支持分片、复制和动态密钥迁移。


性能预览

我们在网站上展示了一些关键结果,将 Garnet 与领先的开源缓存存储进行比较。


设计亮点

Garnet 的设计重新思考了整个缓存存储堆栈——从网络上接收数据包,到解析和处理数据库操作,再到执行存储交互。我们建立在我们之前多年研究的基础上。下面是Garnet的整体架构。

Garnet 的网络层继承了受我们之前对 ShadowFax 研究启发的共享内存设计。 TLS 处理和存储交互在 IO 完成线程上执行,避免了常见情况下的线程切换开销。这种方法允许 CPU 缓存一致性将数据传输到网络,而不是传统的基于随机播放的设计,后者需要在服务器上移动数据。

Garnet的存储设计由两个Tsavorite键值存储组成,它们的命运由统一的操作日志绑定。第一个存储称为“主存储”,针对原始字符串操作进行了优化,并仔细管理内存以避免垃圾收集。第二个也是可选的“对象存储”针对复杂对象和自定义数据类型进行了优化,包括排序集、集合、哈希、列表和地理等流行类型。对象存储中的数据类型利用 .NET 库生态系统来实现其当前的实现。它们存储在内存中的堆上(这使得更新非常高效)并以序列化形式存储在磁盘上。未来,我们计划研究使用统一的索引和日志来简化维护。

Garnet 设计的一个显着特点是其窄腰 Tsavorite 存储 API,用于在顶部实现大型、丰富且可扩展的 RESP API 表面。该 API 包含读取、更新插入、删除和原子读取-修改-写入操作,通过 Garnet 的异步回调实现,以便在每个操作期间的各个点插入逻辑。存储 API 模型使我们能够将 Garnet 的解析和查询处理问题与并发、存储分层和检查点等存储细节完全分开。 Garnet 对多密钥交易使用两阶段锁定。


集群模式

除了单节点执行之外,Garnet还支持集群模式,允许用户创建和管理分片和复制部署。 Garnet 还支持高效、动态的密钥迁移方案来重新平衡分片。用户可以使用标准的Redis集群命令来创建和管理Garnet集群,节点执行gossip来共享和演化集群状态。集群工作仍在进行中。

License

该项目已获得 MIT 许可证的许可,请参阅许可证文件。


隐私

隐私信息可在 https://privacy.microsoft.com/en-us/ 找到。


贡献

该项目欢迎贡献和建议。大多数贡献都要求您同意贡献者许可协议 (CLA),声明您有权并且实际上授予我们使用您的贡献的权利。有关详细信息,请访问 https://cla.opensource.microsoft.com。

当您提交拉取请求时,CLA 机器人将自动确定您是否需要提供 CLA 并适当地修饰 PR(例如状态检查、评论)。只需按照机器人提供的说明进行操作即可。您只需使用我们的 CLA 在所有存储库中执行一次此操作。

该项目采用了微软开源行为准则。有关详细信息,请参阅行为准则常见问题解答或联系 opencode@microsoft.com 提出任何其他问题或意见。


商标

该项目可能包含项目、产品或服务的商标或徽标。 Microsoft 商标或徽标的授权使用须遵守且必须遵循 Microsoft 的商标和品牌指南。在此项目的修改版本中使用 Microsoft 商标或徽标不得引起混淆或暗示 Microsoft 赞助。对第三方商标或徽标的任何使用均须遵守这些第三方的政策。

Redis 是 Redis Ltd 的注册商标。其中的任何权利均归 Redis Ltd 保留。Microsoft 的任何使用仅供参考,并不表明 Redis 与 Microsoft 之间有任何赞助、认可或从属关系。

github地址: https://github.com/microsoft/garnet