Alwayson架构下 服务器 各虚拟IP漂移监控告警的功能实现
1.需求概括
我们知道,在SQL Server Alwayson 架构中,有多种虚拟IP,例如 WindowsCluster IP,ListenIP,角色高可用性IP(类似于侦听IP)。在某些条件下,例如系统故障,会触发虚拟IP的漂移,如何高效率、低延迟、更好地监控IP漂移情况,是我们DB的一个重要工作。
下面是我们的一个通过SQL Server 存储过程来实现的方案。
2.基本原理
周期性收集当前Server 上相应的IP地址,并与上个周期收集的结果比较判断,看那些IP发生了漂移变化。其主要流程图如下;
3.代码实现
表 DBA_ServerIPDataBase_OverCheck,主要存储 当前 (本收集周期) Server的信息(主要是IP信息、ServerName信息等),其创建脚本如下;
/****** Object: Table [dbo].[DBA_ServerIPDataBase_OverCheck] Script Date: 2019/6/27 16:01:27 ******/
SET ANSI_NULLS ON
GO SET QUOTED_IDENTIFIER ON
GO SET ANSI_PADDING ON
GO CREATE TABLE [dbo].[DBA_ServerIPDataBase_OverCheck](
[LocalServerIP] [varchar](20) NULL,
[ClusterName] [varchar](50) NULL,
[ServerIP] [varchar](20) NULL,
[ServerName] [varchar](100) NULL,
[ServerFullName] [varchar](100) NULL,
[ServerIPType] [varchar](20) NULL,
[DataBaseName] [varchar](300) NULL,
[DisabledFlag] [varchar](1) NULL,
[CreateTime] [datetime] NULL,
[CreateBy] [varchar](50) NULL,
[ModifyTime] [datetime] NULL,
[ModifyBy] [varchar](50) NULL
) ON [PRIMARY] GO SET ANSI_PADDING OFF
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Windows集群名称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'ClusterName'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'IP地址' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'ServerIP'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'计算机对象名称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'ServerName'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'计算机对象全称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'ServerFullName'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'计算机对象全称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'ServerIPType'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'0实时有效,1第一次失效,2第二次失效,3第三次失效,4第四次失效,5第五次失效,彻底删除' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'DisabledFlag'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'CreateTime'
GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'DBA_ServerIPDataBase_OverCheck', @level2type=N'COLUMN',@level2name=N'CreateBy'
GO
表 DBA_ServerIPDataBase_OverCheckOriginOrigin,主要存储 以前 (前一个收集周期) Server的信息,是用来比较变化的基准表,其表结构 与表DBA_ServerIPDataBase_OverCheck相同,创建脚本如下:
/****** Object: Table [dbo].[DBA_ServerIPDataBase_OverCheckOriginOrigin] Script Date: 2019/6/27 16:56:28 ******/
SET ANSI_NULLS ON
GO SET QUOTED_IDENTIFIER ON
GO SET ANSI_PADDING ON
GO CREATE TABLE [dbo].[DBA_ServerIPDataBase_OverCheckOriginOrigin](
[LocalServerIP] [varchar](20) NULL,
[ClusterName] [varchar](50) NULL,
[ServerIP] [varchar](20) NULL,
[ServerName] [varchar](100) NULL,
[ServerFullName] [varchar](100) NULL,
[ServerIPType] [varchar](20) NULL,
[DataBaseName] [varchar](300) NULL,
[DisabledFlag] [varchar](1) NULL,
[CreateTime] [datetime] NULL,
[CreateBy] [varchar](50) NULL,
[ModifyTime] [datetime] NULL,
[ModifyBy] [varchar](50) NULL
) ON [PRIMARY] GO SET ANSI_PADDING OFF
GO
具体的存储过程 USP_DBA_ServerIPDataBase_OverCheck,用来实现 收集、比较、告警等功能,代码实现如下:
/****** Object: StoredProcedure [dbo].[USP_DBA_ServerIPDataBase_OverCheck] Script Date: 2019/6/27 15:26:06 ******/
SET ANSI_NULLS ON
GO SET QUOTED_IDENTIFIER ON
GO CREATE PROCEDURE [dbo].[USP_DBA_ServerIPDataBase_OverCheck]
AS
BEGIN DECLARE @ipline VARCHAR(200)
DECLARE @ip VARCHAR(20)
DECLARE @pingname VARCHAR(40)
DECLARE @ServerName VARCHAR(100)
DECLARE @ServerFullName VARCHAR(100) Declare @CurrDateTime nvarchar(20)
Declare @PreDiffDateTime nvarchar(20) ='' Set @CurrDateTime=CONVERT(VARCHAR(19),GETDATE(),120) SET NOCOUNT ON Declare @ServerIP VARCHAR(20)
SET @ServerName=@@SERVERNAME
SET @ServerIP = CAST(CONNECTIONPROPERTY('local_net_address') AS varchar(20)) ----Set @ServerIP ='XXX.XXX.XXX.XXX' --如果不准确的话,请手动定义 DELETE FROM DBA_ServerIPDataBase_OverCheck
WHERE CreateTime< CONVERT(VARCHAR(19),DATEADD( HH,-10,GETDATE()),120) SET @ip = NULL
IF OBJECT_ID('tempdb..#tempserverip') IS NOT NULL
DROP TABLE #tempserverip
CREATE TABLE #tempserverip ( ipline VARCHAR(200) )
INSERT #tempserverip
EXEC master..xp_cmdshell 'ipconfig /all ' INSERT INTO DBA_ServerIPDataBase_OverCheck
( LocalServerIP,ServerIP,DisabledFlag,CreateTime,CreateBy
)
SELECT @ServerIP, p.ServerIP,''AS DisabledFlag,GETDATE() AS CreateTime ,@@SERVERNAME AS CreateBy
FROM dbo.DBA_ServerIPDataBase_OverCheck i
RIGHT JOIN ( SELECT RTRIM(LTRIM(REPLACE(SUBSTRING(ipline,
CHARINDEX(':',
ipline) + 1, 20),
'(首选)', ''))) AS 'ServerIP'
FROM #tempserverip
WHERE UPPER(ipline) LIKE '%IPv4 地址%'--这里需要注意一下,系统不同这里的匹配值就不同
AND UPPER(ipline) NOT LIKE '%192.168.%'
AND UPPER(ipline) NOT LIKE '%169.254.%'
) p ON i.ServerIP = p.ServerIP
WHERE i.ServerIP IS NULL --只关注漂来飘往数据 --0002 -20180530 针对20180530持续告警问题,发现告警时间超过预期,进行优化。聚焦点再表DBA_ServerIPDataBase_OverCheck中的CreateTime栏位,精准更新 begin
update i set i.CreateTime=getdate()
FROM dbo.DBA_ServerIPDataBase_OverCheck i
RIGHT JOIN ( SELECT RTRIM(LTRIM(REPLACE(SUBSTRING(ipline,
CHARINDEX(':',
ipline) + 1, 20),
'(首选)', ''))) AS 'ServerIP'
FROM #tempserverip
WHERE UPPER(ipline) LIKE '%IPv4 地址%'--这里需要注意一下,系统不同这里的匹配值就不同
AND UPPER(ipline) NOT LIKE '%192.168.%'
AND UPPER(ipline) NOT LIKE '%169.254.%'
) p ON i.ServerIP = p.ServerIP ------
DECLARE IP CURSOR
FOR
SELECT ServerIP
FROM dbo.DBA_ServerIPDataBase_OverCheck WHERE DisabledFlag IS NOT NULL AND DisabledFlag=0
OPEN IP
FETCH NEXT FROM IP INTO @ip
WHILE @@FETCH_STATUS = 0
BEGIN
--SET @pingname = 'ping -a ' + @ip
SET @pingname = 'ping -a ' + @ip + ' -n 1 -l 10' TRUNCATE TABLE #tempserverip
INSERT #tempserverip
EXEC master..xp_cmdshell @pingname SELECT @ServerName = REPLACE(RTRIM(LTRIM(SUBSTRING(ipline, 8,
CHARINDEX('[',
ipline) - 8))),
'.XXXXXX.com', '') ,-----加域的电脑,计算机名字可能带有域名,请根据实际情况替换
@ServerFullName = RTRIM(LTRIM(SUBSTRING(ipline, 8,
CHARINDEX('[',
ipline) - 8)))
FROM #tempserverip
WHERE ipline LIKE '%正在 Ping%' UPDATE dbo.DBA_ServerIPDataBase_OverCheck
SET ServerName = @ServerName ,
ServerFullName = @ServerFullName
WHERE ServerIP = @ip FETCH NEXT FROM IP INTO @ip
END
CLOSE IP
DEALLOCATE IP UPDATE dbo.DBA_ServerIPDataBase_OverCheck
SET ServerIPType = 'Localhost'
WHERE ServerName = @@SERVERNAME if OBJECT_ID('sys.availability_group_listener_ip_addresses') IS NOT NULL
begin
update a set a.ServerIPType='ListenIP'
from DBA_ServerIPDataBase_OverCheck a inner join sys.availability_group_listener_ip_addresses b
on a.ServerIP=b.ip_address
end IF OBJECT_ID('sys.dm_hadr_cluster') IS NOT NULL
BEGIN
UPDATE dbo.DBA_ServerIPDataBase_OverCheck SET ClusterName=(SELECT cluster_name FROM sys.dm_hadr_cluster)
UPDATE dbo.DBA_ServerIPDataBase_OverCheck SET ServerIPType='WindowsCluster' WHERE ServerName=(SELECT cluster_name FROM sys.dm_hadr_cluster)
END
IF OBJECT_ID('sys.dm_hadr_cluster') IS NULL
BEGIN
UPDATE dbo.DBA_ServerIPDataBase_OverCheck SET ClusterName='Not Cluster'
END
--0002 -20180530 针对20180530持续告警问题,发现告警时间超过预期,进行优化。聚焦点再表DBA_ServerIPDataBase_OverCheck中的CreateTime栏位,精准更新 begin
UPDATE DBA_ServerIPDataBase_OverCheck SET DataBaseName=STUFF(
(SELECT ',' + name FROM sys.databases
WHERE name not in ('master'
,'tempdb','model','msdb','ReportServer','ReportServerTempDB','distribution')
for xml path('') ),
1,1,'')
-----002 end
--the mail alarm
declare @SQL as varchar(200)
declare @Subject as varchar(200)=N'DB SERVER IP 有漂移,请检查确认!'
declare @Body as nvarchar(max)='' select @PreDiffDateTime= CreateTime from DBA_ServerIPDataBase_OverCheckOriginOrigin
order by CreateTime SELECT TOP 0 A.* into #temp_DBA_ServerIPDataBase_OverCheck_diff FROM DBA_ServerIPDataBase_OverCheck A INNER JOIN DBA_ServerIPDataBase_OverCheckOriginOrigin B
ON A.ServerIP =B.ServerIP IF EXISTS(SELECT * FROM DBA_ServerIPDataBase_OverCheck A LEFT JOIN DBA_ServerIPDataBase_OverCheckOriginOrigin B ON A.ServerIP =B.ServerIP WHERE B.ServerIP IS NULL)
BEGIN INSERT INTO #temp_DBA_ServerIPDataBase_OverCheck_diff
SELECT A.* FROM DBA_ServerIPDataBase_OverCheck A LEFT JOIN DBA_ServerIPDataBase_OverCheckOriginOrigin B ON A.ServerIP =B.ServerIP WHERE B.ServerIP IS NULL if exists( select * from #temp_DBA_ServerIPDataBase_OverCheck_diff )
begin
set @Body= N'<html>'
+ N'<style type="text/css">'
+ N' td {border:solid #9ec9ec; border-width:1px 1px 1px 1px; padding:4px 0px;}'
+ N' table {border:1px solid #9ec9ec;width:80%;border-width:0px 0px 0px 0px;font-size:14px}'
+ N'</style>'
+ N'<H1 style="color:#FF0000;font-size:14px"></H1>'
SET @Body=@Body+'<body><font color=#0000CC>Dear All,<br><br> 此List是监控到过去10 MIn Server IP 异常情况,'+@ServerIP +'服务器有新IP创建声明(新增), 请及时Check。具体数据如下:;<br><br><table>'
SET @Body=@Body+'<tr bgcolor=#FFFF00 align="center"><td>ClusterName</td><td>ServerIP</td><td>ServerName</td><td>ServerIPType</td><td>DatabaseName</td><td>DisabledFlag</td><td>当前时间</td><td>差异采样时间</td></tr>'
SELECT @Body=@Body+'<tr><td>'+ClusterName+'</td><td>'+ServerIP+'</td><td>'+ServerName+'</td><td>'+ServerIPType+'</td><td>'+DatabaseName+'</td><td>'+ DisabledFlag+'</td><td>'+@CurrDateTime+'</td><td>'+ @PreDiffDateTime+'</td></tr>'
from #temp_DBA_ServerIPDataBase_OverCheck_diff SET @Body = @Body +'</table><font color=#0000CC><br><br>DBA<br>Best wishes</body><html>'
end
END IF EXISTS(SELECT * FROM DBA_ServerIPDataBase_OverCheck A RIGHT JOIN DBA_ServerIPDataBase_OverCheckOriginOrigin B ON A.ServerIP =B.ServerIP WHERE A.ServerIP IS NULL)
BEGIN DELETE FROM #temp_DBA_ServerIPDataBase_OverCheck_diff
INSERT INTO #temp_DBA_ServerIPDataBase_OverCheck_diff
SELECT A.* FROM DBA_ServerIPDataBase_OverCheck A RIGHT JOIN DBA_ServerIPDataBase_OverCheckOriginOrigin B ON A.ServerIP =B.ServerIP WHERE A.ServerIP IS NULL if exists( select * from #temp_DBA_ServerIPDataBase_OverCheck_diff )
begin
set @Body= N'<html>'
+ N'<style type="text/css">'
+ N' td {border:solid #9ec9ec; border-width:1px 1px 1px 1px; padding:4px 0px;}'
+ N' table {border:1px solid #9ec9ec;width:80%;border-width:0px 0px 0px 0px;font-size:14px}'
+ N'</style>'
+ N'<H1 style="color:#FF0000;font-size:14px"></H1>'
SET @Body=@Body+'<body><font color=#0000CC>Dear All,<br><br> 此List是监控到过去10 MIn Server IP 异常情况,'+@ServerIP +'服务器有IP漂移(消减), 请及时Check。具体数据如下:;<br><br><table>'
SET @Body=@Body+'<tr bgcolor=#FFFF00 align="center"><td>ClusterName</td><td>ServerIP</td><td>ServerName</td><td>ServerIPType</td><td>DatabaseName</td><td>DisabledFlag</td><td>当前时间</td><td>差异采样时间</td></tr>'
SELECT @Body=@Body+'<tr><td>'+ClusterName+'</td><td>'+ServerIP+'</td><td>'+ServerName+'</td><td>'+ServerIPType+'</td><td>'+DatabaseName+'</td><td>'+ DisabledFlag+'</td><td>'+@CurrDateTime+'</td><td>'+ @PreDiffDateTime+'</td></tr>'
from #temp_DBA_ServerIPDataBase_OverCheck_diff SET @Body = @Body +'</table><font color=#0000CC><br><br>DBA<br>Best wishes</body><html>'
end END
SET @BODY=REPLACE(@BODY,'''','')
IF REPLACE(@BODY,' ','')<>''
BEGIN Declare @AllEmailToAddress varchar(3000)=''
Declare @AllEmailCcAddress varchar(3000)=''
Select @AllEmailToAddress='hanmeimei;xiaoming;lilei' Select @AllEmailCcAddress='laoban' exec msdb..sp_send_dbmail @profile_name = 'AutoMail' -- profile 名称,请检查此参数,根据实际情况进行替换
,@recipients = @AllEmailToAddress -- 收件人邮箱
,@copy_recipients=@AllEmailCcAddress
,@subject = @Subject -- 邮件标题
,@body = @BODY -- 邮件内容
,@body_format = 'HTML' -- 邮件格式
,@file_attachments=''
,@importance = 'HIGH' -- varchar(10) 告警级别
END ------------------新增立即插入----------- insert into [dbo].[DBA_ServerIPDataBase_OverCheckOriginOrigin]
select a.* from DBA_ServerIPDataBase_OverCheck a left join DBA_ServerIPDataBase_OverCheckOriginOrigin b
on a.ServerIP=b.ServerIP
where b.ServerIP is null ---漂移后,指定时间段后直接删除过时数据,暂定八个小时。【即如果有漂移(减少),减少的IP信息,则在指定时间后,删除。】 delete b
from DBA_ServerIPDataBase_OverCheck a right join DBA_ServerIPDataBase_OverCheckOriginOrigin b
on a.ServerIP=b.ServerIP
where a.ServerIP is null
and b.CreateTime< CONVERT(VARCHAR(19),DATEADD( HH,-3,GETDATE()),120) -----将数据插入到远程Server DB中,远程Server有一个SP过程,用来判断漂移前/后DB是否有变化。远程的SP主要是依据 LocalServerIP 和 ServerIP 对应关系变化情况来判断。这段代码省略,下次再描述 SET NOCOUNT OFF End GO
4.功能实现
例如当 服务器有新IP创建声明(新增)时,其发出的告警邮件如下:
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
Alwayson架构下 服务器 各虚拟IP漂移监控告警的功能实现的更多相关文章
- SQL Server Alwayson架构下 服务器 各虚拟IP漂移监控告警的功能实现 -2(虚拟IP视角)
1.需求描述 我们知道Windows Cluster 都是多节点的,当虚拟IP漂移的时候,一般都是从一个节点漂移到另外一个节点.如果可以及时捕捉到旧节点信息是什么.新节点信息是什么对我们提供高可用的数 ...
- linux下如何设置vip(虚拟ip)
在做HA的时候需要为服务器设计虚拟IP,也就是一个主机对应多个IP地址?刚听起来好神奇,原来这样也是可能的看了下面的这个链接 自己配了一下http://hi.baidu.com/pbottle/ite ...
- 微服务管理平台nacos虚拟ip负载均衡集群模式搭建
一.Nacos简介 Nacos是用于微服务管理的平台,其核心功能是服务注册与发现.服务配置管理. Nacos作为服务注册发现组件,可以替换Spring Cloud应用中传统的服务注册于发现组件,如:E ...
- Keepalived虚拟ip
linux下如何设置vip(虚拟ip) 在做HA的时候需要为服务器设计虚拟IP,也就是一个主机对应多个IP地址?刚听起来好神奇,原来这样也是可能的看了下面的这个链接 自己配了一下http://hi.b ...
- 以用户注册功能模块为例浅谈MVC架构下的JavaWeb开发流程
JavaWeb应用开发,撇开分布式不谈,只讨论一个功能服务应用的开发,无论是使用原生的Servlet/JSP方案,还是时下的SSM架构,都有一套经过工程实践考验的最佳实践,这综合考虑了团队协作.项目管 ...
- 虚拟IP和IP漂移
学习一下虚拟IP和IP漂移的概念. 1.虚拟IP 在 TCP/IP 的架构下,所有想上网的电脑,不论是用何种方式连上网路,都必须要有一个唯一的 IP-address.事实上IP地址是主机硬件地址的一种 ...
- 虚拟IP技术 ip地址漂移技术
虚拟IP地址(VIP) 是一个不与特定计算机或一个计算机中的网络接口卡(NIC)相连的IP地址.数据包被发送到这个VIP地址,但是所有的数据还是经过真实的网络接口.VIPs大部分用于连接冗余:一个VI ...
- MVC项目实践,在三层架构下实现SportsStore-08,部署到IIS服务器
SportsStore是<精通ASP.NET MVC3框架(第三版)>中演示的MVC项目,在该项目中涵盖了MVC的众多方面,包括:使用DI容器.URL优化.导航.分页.购物车.订单.产品管 ...
- Windows环境下使用bitvise架构sftp服务器
Windows环境下使用Bitvise架构sftp服务器 Bitvise分成Bitvise ssh server和Bitvise ssh client 0 说明:文档暂时只使用Bitvise工具做搭建 ...
随机推荐
- WinForm - 窗体淡入效果界面的简单实现方法
WinForm窗体淡入效果主要使用到控件的Opacity属性 首先在WinForm窗体中拖入一个Timer控件,然后再Timer控件的Tick事件添加如下代码: private void timer1 ...
- CUDA中的常量内存__constant__
GPU包含数百个数学计算单元,具有强大的处理运算能力,可以强大到计算速率高于输入数据的速率,即充分利用带宽,满负荷向GPU传输数据还不够它计算的.CUDA C除全局内存和共享内存外,还支持常量内存,常 ...
- Android中使用sqlite3操作SQLite
SQLite库包含一个名字叫做sqlite3的命令行,它可以让用户手工输入并执行面向SQLite数据库的SQL命令.本文档提供一个样使用sqlite3的简要说明. 一.创建数据库: 1.将sqlit ...
- Android中使用ListView实现自适应表格
GridView比ListView更容易实现自适应的表格,但是GridView每个格单元的大小固定,而ListView实现的表格可以自定义每个格单元的大小,但因此实现自适应表格也会复杂些(格单元大小不 ...
- Android 悬浮窗、悬浮球开发
原文:Android 悬浮窗.悬浮球开发 1.权限管理 直接看我另外一篇博客吧,传送门: https://my.oschina.net/u/1462828/blog/1933162 2.Base类Ba ...
- Android笔记--自定义控件仿遥控器的圆形上下左右OK圆盘按钮
原文:Android笔记--自定义控件仿遥控器的圆形上下左右OK圆盘按钮 上面就是几张预览图!代码在最底下 主要就两个步骤,画图.监听点击 1.整个控件基本上是一步步画出来的,重写onDraw方法开始 ...
- WPF 自定义的图表(适用大量数据绘制)下
原文:WPF 自定义的图表(适用大量数据绘制)下 上一篇文章中讲了WPF中自定义绘制大量数据的图标,思路是先将其绘制在内存,然后一次性加载到界面,在后续的调试过程中,发现当数据量到达10W时,移动鼠标 ...
- 【值转换器】 WPF中Image数据绑定Icon对象
原文:[值转换器] WPF中Image数据绑定Icon对象 这是原来的代码: <Image Source="{Binding MenuIcon}" ...
- 大约Android远程监控APP源代码
这篇文章的目的,关心询问名人,要打开源代码.这里说明,远程监控摄像头场外,相反,用手机摄像头摄像头server上,要理解这一点.关于非常网上的文章达到server道路.它能够准确,念就乱发博文,当然假 ...
- Bootstrap 反色导航条
@{ Layout = null;}<!DOCTYPE html><html><head> <meta name="viewport&q ...