SpringCloud集成sleuth和zipkin实现微服务链路追踪
文章目录
- 前言
- 技术积累
- spring cloud sleuth介绍
- zipkin介绍
- Zipkin与Sleuth的协作
- SpringCloud多模块搭建
- Zipkin Server部署
- docker pull 镜像
- 启动zipkin server
- SpringCloud 接入 Sleuth 与 Zipkin
- pom引入依赖 (springboot2.6+)
- appilication.yml配置修改
- 增加测试链路代码
- 调用微服务进行验证
前言
最近项目上准备引入接口调用链路追踪,说到这个我们就不得不想到springcloud全家桶中的sleuth了。他可以将跨多个服务请求链路记录下来,供我们查询分析。然后,我们在此基础上用zipkin来采集和上报分析请求链路,简直不要太爽。那么今天就分享一期微服务架构接入sleuth+zipkin实战演示。
技术积累
spring cloud sleuth介绍
Sleuth是Spring cloud的分布式跟踪解决方案。

1.span(跨度),基本工作单元。一次链路调用,创建一个span,span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanld,span父id.span被启动和停止时,记录了时间信息,初始化span叫:rootspan,它的span id和trace id相等。
2.trace(跟踪),一组共享"root span”的span组成的树状结构称为 trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。
3.annotation (标签),annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束CS(Client Send客户端发起请求)。客户端发起请求描述了span开始,
SR(Server Received服务端接到请求)。服务端获得请求并准备处理它。SR-CS=网络延迟SS(Server Send服务器端处理完成,并将结果发送给客户端)。表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间,
CR(Client Received 客户端接受服务端信息)。span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。
其实数据结构是一颗树,从root span 开始。
工具定位
Spring Cloud Sleuth是一个用于Spring Cloud应用程序的分布式追踪工具,它主要专注于在服务间传播追踪信息。
主要功能
传播追踪上下文:在服务调用间传递Trace ID和Span ID,确保整个请求链路的追踪信息保持一致。
集成日志框架:修改日志框架的配置,使得日志记录中包含Trace和Span的ID。
与Spring Cloud生态集成:与Spring Cloud的其他组件(如Ribbon、Hystrix、Zuul等)无缝集成。
依赖性:Sleuth是为Spring Cloud应用程序设计的,它依赖于Spring框架和Spring Boot。
zipkin介绍
Zipkin是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper的论文设计而来,由 Twitter 公司开发贡献。其主要功能是聚集来自各个异构系统的实时监控数据。
工具定位
Zipkin是一个分布式追踪系统,它提供了追踪数据的收集、存储、查找和展示功能。
主要功能
收集追踪数据:Zipkin通过其客户端库(如Brave)收集追踪数据。
存储追踪数据:支持多种存储方案,如内存、MySQL、Cassandra、Elasticsearch等。
查询和展示:提供Web界面,用于查询追踪数据,并以可视化的方式展示服务间的调用关系和延迟。
独立性:Zipkin可以独立于任何应用程序或框架运行,并且可以与多种编程语言和框架集成。
zipkin 官网
https://zipkin.io/pages/quickstart.html

Zipkin与Sleuth的协作
数据收集:Sleuth负责在Spring Cloud应用程序中生成和传播追踪数据,而Zipkin负责收集这些数据。
数据展示:Sleuth生成的追踪数据可以通过Zipkin的Web界面进行查询和展示。
集成:在Spring Cloud应用程序中,Sleuth通常与Zipkin一起使用,Sleuth负责追踪信息的生成和传播,Zipkin负责存储和展示。
SpringCloud多模块搭建
多模块微服务项目结构

新增父项目来管理微服务各个模块


#父模块pom必须声明
pom
新增子模块

Zipkin Server部署
由于springcloud F版本不支持自定义zipkin server,故我们采用docker镜像进行部署zipkin server。
docker pull 镜像
docker pull openzipkin/zipkin:latest
启动zipkin server
未持久化(内存):
docker run --name zipkin-server -d --restart=always -p 9411:9411 openzipkin/zipkin:latest
http://127.0.0.1:9411/zipkin/
持久化到MySQL5:
docker run --name zipkin-server-mysql -d --restart=always -p 19411:9411 -e MYSQL_USER=root -e MYSQL_PASS=12345678 -e MYSQL_HOST=127.0.0.1 -e STORAGE_TYPE=mysql -e MYSQL_DB=zipkin -e MYSQL_TCP_PORT=13306 openzipkin/zipkin:3.3
mysql DD语句如下:
https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql
--
-- Copyright The OpenZipkin Authors
-- SPDX-License-Identifier: Apache-2.0
--CREATE TABLE IF NOT EXISTS zipkin_spans (`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',`trace_id` BIGINT NOT NULL,`id` BIGINT NOT NULL,`name` VARCHAR(255) NOT NULL,`remote_service_name` VARCHAR(255),`parent_id` BIGINT,`debug` BIT(1),`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';CREATE TABLE IF NOT EXISTS zipkin_annotations (`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';CREATE TABLE IF NOT EXISTS zipkin_dependencies (`day` DATE NOT NULL,`parent` VARCHAR(255) NOT NULL,`child` VARCHAR(255) NOT NULL,`call_count` BIGINT,`error_count` BIGINT,PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
持久化到ES:
docker run --name zipkin-server-es -d -p 19411:9411 --restart=always -e STORAGE_TYPE=elasticsearch -e ES_HOSTS=127.0.0.1:9200 openzipkin/zipkin:latest
JAR运行:
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=12345678
我们测试选择不持久化
docker run -d -p 9411:9411 --name zipkinServer --restart=always openzipkin/zipkin
3、启动完成


SpringCloud 接入 Sleuth 与 Zipkin
pom引入依赖 (springboot2.6+)
<properties><spring-cloud.version>2021.0.5</spring-cloud.version>
</properties>
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId><version>3.1.10</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-sleuth-zipkin</artifactId><version>3.1.10</version></dependency>
</dependencies>
appilication.yml配置修改
spring:zipkin:#zipkin服务所在地址base-url: http://127.0.0.1:9411/sender:type: web #使用http的方式传输数据#配置采样百分比sleuth:sampler:probability: 1 # 将采样比例设置为 1.0,也就是全部都需要。默认是0.1也就是10%,一般情况下,10%就够用了
##打开debug日志
logging:level:org.springframework.web.servlet.DispatcherServlet: Debug
type=web也就是通过 HTTP 的方式发送数据到 Zipkin 。
如果请求量比较大,这种方式其实性能是比较低的,一般情况下我们都是通过消息中间件来发送,比如 RabbitMQ 。如果日志数据量比较大,一般推荐拥有更高吞吐量的 Kafka 来进行日志推送。这种方式就是让服务将 Sleuth 收集的日志推给 MQ ,让 Zipkin 去监控 MQ 的信息,通过 MQ 的队列获取到服务的信息。这样就提高了性能。
而日志的存储则可以采用mysql、 Elasticsearch 对数据进行持久化,这样可以保证 Zipkin 重启后,链路信息不会丢失。
增加测试链路代码
链路为:
user-service --> order-service --> finance-service
user-service增加请求入口getOrderAmount:
/*** 计算订单金额* @param orderId* @author senfel* @date 2024/12/9 18:25* @return java.lang.String*/
@RequestMapping("/getOrderAmount")
public String getOrderAmount(@RequestParam(name = "orderId") String orderId) {return orderService.getOrderAmount(orderId);
}
user-service增加feign调用order-service:
/*** FeeService* @author senfel* @version 1.0* @date 2024/12/9 18:02*/
@FeignClient(name = "order-service",fallback = OrderServiceFallback.class)
public interface getOrderAmount {/*** 获取订单金额* @param orderId* @author senfel* @date 2024/12/9 18:06* @return java.lang.String*/@RequestMapping(value = "/order/getOrderAmount",method = RequestMethod.GET)String getOrderAmount(@RequestParam(name = "orderId") String orderId);
}
order-service增加调用入口getOrderAmount:
/*** 获取订单金额* @param orderId* @author senfel* @date 2024/12/9 18:06* @return java.lang.String*/
@RequestMapping(value = "/getOrderAmount",method = RequestMethod.GET)
public String getOrderAmount(@RequestParam(name = "orderId") String orderId){return financeService.getFee(orderId);
}
order-service增加feign调用finance-service:
/*** FinanceService* @author senfel* @version 1.0* @date 2024/12/9 18:10*/
@FeignClient(name = "finance-service",fallback = FinanceServiceFallback.class)
public interface FinanceService {/*** 获取费用* @param orderId* @author senfel* @date 2024/12/9 18:12* @return java.lang.String*/@RequestMapping(value = "/finance/getFee",method = RequestMethod.GET)String getFee(@RequestParam(name = "orderId") String orderId);
}
finance-service增加请求入口getFee:
/*** 获取费用* @param orderId* @author senfel* @date 2024/12/9 18:12* @return java.lang.String*/
@RequestMapping(value = "/getFee",method = RequestMethod.GET)
public String getFee(@RequestParam(name = "orderId") String orderId) {return "100";
}
调用微服务进行验证
启动微服务调用getOrderAmount()

进入zipkin界面查看请求调用链路和各个阶段耗时



至此,我们已经完微服务链路追踪实战.。
相关文章:
SpringCloud集成sleuth和zipkin实现微服务链路追踪
文章目录 前言技术积累spring cloud sleuth介绍zipkin介绍Zipkin与Sleuth的协作 SpringCloud多模块搭建Zipkin Server部署docker pull 镜像启动zipkin server SpringCloud 接入 Sleuth 与 Zipkinpom引入依赖 (springboot2.6)appilication.yml配置修改增加测试链路代码 调用微服…...
Python随机抽取Excel数据并在处理后整合为一个文件
本文介绍基于Python语言,针对一个文件夹下大量的Excel表格文件,基于其中每一个文件,随机从其中选取一部分数据,并将全部文件中随机获取的数据合并为一个新的Excel表格文件的方法。 首先,我们来明确一下本文的具体需求。…...
Linux+Docker onlyoffice 启用 HTTPS 端口支持
文章目录 一、需求二、配置2.1 创建容器2.2 进入容器2.3 生成私钥和证书 2.4 测试访问 一、需求 上篇文章介绍了如何搭建一个 onlyoffice 在线预览服务,但是我们实际场景调用该服务的网站是协议是 https 的 ,但是 onlyoffice 服务还没做配置,…...
在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c
在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c 1. Installing the extension (在 Visual Studio Code 中安装插件)1.1. Extensions for Visual Studio Code1.2. C/C1.2.1. Pre-requisites 1.3. Makefile Tools 2. Configuring your project (配置项目)2.1.…...
python中math模块常用函数
文章目录 math模块简介各种三角函数反三角函数取整函数欧几里得距离绝对值最大公约数开根号幂阶乘函数 math模块简介 math模块是python标准库的一部分,提供了对于浮点数相关的数学运算,下面是常用的一些function 各种三角函数反三角函数 math.cos、ma…...
优化 Vue 3 开发体验:配置 Vite 使用 WebStorm 作为 Vue DevTools 的默认编辑器
优化 Vue 3 开发体验:配置 Vite 使用 WebStorm 替代 VS Code 作为 Vue DevTools 的默认编辑器 在 Vue 3 项目开发中,合理配置开发工具可以大大提升我们的工作效率。本文将介绍如何配置 Vite,使其在使用 Vue DevTools 时将默认编辑器从 VS Co…...
【C语言练习(9)—有一个正整数,求是几位数然后逆序打印】
C语言练习(9) 文章目录 C语言练习(9)前言题目题目解析结果总结 前言 主要到整数的取余(%)和整数的取商(/),判断语句if…else if …else的使用 题目 给一个不多于3位的正整数,要求:一、求它是几位数&…...
热敏打印机的控制
首次接触热敏打印机,本来没有特别之处,花了大概十天时间完成一款猫学王热敏打印机,给到客户体验后,客户反馈说打字看起来不明显,打印照片有条纹,所以引起了我对于他的关注,几点不足之处需要优化…...
【closerAI ComfyUI】电商赋能,AI模特套图生产,各种姿势自定义,高度保持人物服饰场景一致性,摆拍街拍专用
closerAIGCcloserAI,一个深入探索前沿人工智能与AIGC领域的资讯平台,我们旨在让AIGC渗入我们的工作与生活中,让我们一起探索AIGC的无限可能性!aigc.douyoubuy.cn 【closerAI ComfyUI】电商赋能,AI模特套图生产,各种姿势自定义,高度保持人物服饰场景一致性,摆拍街拍专用…...
ARM学习(36)静态扫描规则学习以及工具使用
笔者来学习了解一下静态扫描以及其规则,并且亲身是实践一下对arm 架构的代码进行扫描。 1、静态扫描认识 静态扫描:对代码源文件按照一定的规则进行扫描,来发现一些潜在的问题或者风险,因为不涉及代码运行,所以其一般只是发现一些规范或则一些质量问题,当然这些可能存在潜…...
使用 Docker Compose 部署 Redis 主从与 Sentinel 高可用集群
文章目录 使用 Docker Compose 部署 Redis 主从与 Sentinel 高可用集群Redis 主从架构简介Redis Sentinel 简介配置文件1. 主节点配置 (redis-master.conf)2. 从节点配置 (redis-slave1.conf 和 redis-slave2.conf)redis-slave1.confredis-slave2.conf3. Sentinel 配置 (sentin…...
警惕!手动调整服务器时间可能引发的系统灾难
警惕!手动调整服务器时间可能引发的系统灾难 1. 鉴权机制1.1 基于时间戳的签名验证1.2 基于会话的认证机制(JWT、TOTP) 2. 雪花算法生成 ID 的影响2.1 时间戳回拨导致 ID 冲突2.2 ID 顺序被打乱 3. 日志记录与审计3.1 日志顺序错误3.2 审计日…...
MySQL追梦旅途之性能优化
1、索引优化 索引可以显著加速查询操作,但过多或不适当的索引也会带来负面影响(如增加写入开销)。因此,选择合适的索引至关重要。 创建索引: 为经常用于WHERE子句、JOIN条件和ORDER BY排序的列创建索引。 CREATE I…...
【机器学习】【无监督学习——聚类】从零开始掌握聚类分析:探索数据背后的隐藏模式与应用实例
从零开始掌握聚类分析:探索数据背后的隐藏模式与应用实例 基本概念聚类分类聚类算法的评价指标(1)内部指标轮廓系数(Silhouette Coefficient)DB指数(Davies-Bouldin Index)Dunn指数 (…...
基于深度Q网络(Deep Q-Network,DQN)的机器人路径规划,可以自定义地图,MATLAB代码
深度Q网络(Deep Q-Network,DQN)是一种结合了深度学习和Q学习的强化学习算法,由DeepMind在2015年提出。 1. 算法介绍 DQN算法通过使用深度神经网络来近似Q值函数,解决了传统Q-learning在处理具有大量状态和动作的复杂…...
Python-从文件中读取数据-Sat-Sun
10.1 文件读取数据可以整个文件读取,也可以逐行读取。 首先在保存有.py文件的文件夹里创建一个pi_digist.txt文件,文件内容是 3.14 9265 3589执行程序 file_reader.py with open(pi_digist.txt) as file_object: #接受文件名参数,在程序所…...
测试工程师的职业规划
测试人员在管理上的发展 基层测试管理者:测试组长 工作内容:安排小组工作,提升小组成员测试能力,负责重要的测试工作。 负责对象:版本,项目 中层测试管理者:测试经理 负责对象࿱…...
使用 Puppeteer 快速上手 Node.js 爬虫
使用 Puppeteer 库通过自动化浏览器来访问百度图片搜索,并在搜索结果中下载图片。代码分为两部分: 自动化浏览器任务:使用 Puppeteer 浏览百度图片搜索并获取图片 URL。图片下载:检查图片 URL 类型(base64 或 URL&…...
浏览器的跨域问题与解决方案
浏览器的跨域问题与解决方案 浏览器的跨域问题源于同源策略(Same-Origin Policy)这一安全机制。同源策略要求两个页面具有相同的协议、域名和端口号,才能相互访问资源和数据。这一机制旨在防止恶意网站执行跨站脚本攻击,从而保护…...
MyBatis一二级缓存的区别?
大家好,我是锋哥。今天分享关于【MyBatis一二级缓存的区别?】面试题。希望对大家有帮助; MyBatis一二级缓存的区别? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 MyBatis 的缓存机制分为 一级缓存 和 二级缓存&…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
