Web应用高级故障排除、维护及异步操作指南
1. 高级故障排除与维护
在Web应用的管理和维护中,有许多关键任务需要处理,以确保系统的稳定运行和数据安全。
1.1 远程维护接口
建议创建基于Web的接口来执行特定的维护任务,如清除请求日志。这样可以通过HTTP远程运行这些任务,在无法现场操作时非常方便。
1.2 数据备份
数据备份是保障数据安全的重要措施。对于数据的定期(可能是每日)磁带备份,虽然有些备份系统声称能在Visual FoxPro表文件打开时进行可靠备份,但为了确保数据的完整性,建议在备份时关闭所有表。不过,如果数据存储在SQL Server或Oracle等后端数据库中,这个问题就不存在了。
为了实现备份时关闭表的操作,可以指定一个维护间隔,通过代码或INI文件设置来拒绝在这段时间内对数据表进行操作。以下是一个示例代码,可放置在主程序的 Process() 方法开头:
LOCAL lcStartMaint, lcEndMaint
lcStartMaint = "02:45" && or read a value from INI
lcEndMaint = "03:15"
IF BETWEEN( TIME(), m.lcStartMaint, m.lcEndMaint)
CLOSE DATA ALL
LOCAL Response
Response = CREATE( "wwResponseString")
Response.StandardPage( "Maintenance Advisory", "Sorry, but we are " + ;
"performing maintenance right now. Check back after " + ;
m.lcEndMaint + " (eastern time). Thank you for your patience.")
IF THIS.lCOMObject
THIS.cOutput=Response.GetOutput()
ELSE
File2Var(THIS.oRequest.GetOutputFile(),Response.GetOutput())
ENDIF
RETURN && avoid any further processing!
ENDIF
这个方法虽然可行,但可能并不总是有效,因为它依赖于在进入维护间隔后、备份设备启动前收到请求。另一种解决方案是在批处理文件中进行备份,在发出备份命令之前向应用程序发送HTTP请求,从而关闭表。但这是一个比较粗糙和不完善的方法。为了实现可靠的备份,要么确保备份期间应用程序不运行,要么将数据迁移到后端数据库。
如果使用Visual FoxPro数据且无法设置维护窗口(即应用程序必须24x7运行),可以使用Visual FoxPro的 COPY TO 命令制作数据的实时副本,然后对这些不需要打开的副本进行磁带备份。更多关于此方法的讨论可参考 相关链接 。
1.3 使用COM进行在线维护
如果使用COM而不是基于文件的消息传递,在执行维护任务时会有更多的灵活性。在COM模式下, admin.asp 页面有一个特殊链接,可选择“卸载除一个服务器外的所有服务器并暂停请求”。执行此操作后,一次只能处理一个请求,并且只接受管理员的请求(由 WC.INI 中的 AdminUser 设置决定)。这使得可以执行像 PACK 和 REINDEX 这样需要独占使用的操作。完成后,释放“暂停”状态,其他用户就可以再次访问应用程序。在暂停期间,用户会看到一个(可自定义的)消息,提示正在进行维护。
要实现这个策略,可以创建Web Connection的 Process() 方法来执行维护任务,从而可以从Web页面调用这些方法。例如,一个打包表的方法可以如下:
FUNCTION PackCustomer
CLOSE DATABASES
IF OpenExclusive( "Customer")
PACK
USE
THIS.StandardPage( "Customer table was packed.")
ELSE
THIS.ErrorMsg( "Could not get exclusive use of Customer table.")
ENDIF
ENDFUNC && PackCustomer
服务器操作员可以按照以下步骤执行维护任务:
1. 通过选择 admin.asp 中的“卸载除一个服务器外的所有服务器并暂停请求”链接(等效查询字符串为“_maintain~maintholdrequests”)将服务器置于维护模式。
2. 执行一个或多个维护功能,如前面所示的打包操作。理想情况下,可以创建一个作为管理员功能菜单的页面,并在 admin.asp 页面和维护菜单之间添加链接,以便轻松导航所有维护任务。
3. 完成所有任务后,通过选择 admin.asp 中的“从INI加载所有服务器”链接(等效查询字符串为“_maintain~load”)或点击Web Connection状态页面上的“切换”切换链接(等效查询字符串为“_maintain~HoldRequests”)释放暂停状态。
需要注意的是,有些维护任务可能会超过标准Web浏览器的超时时间,这些任务不应该从基于Web的维护页面同步执行。
1.4 查找表和元数据的在线维护
对于需要在线维护的查找表和元数据,通常需要开发自己的基于Web的数据输入表单和 Process() 方法。从Web Connection 3.65版本开始, wwShowCursor 类有一个 EditTable() 方法,可用于快速轻松地编辑表,这对于维护功能非常有用。可以参考 wwDemo.PRG 中的 TableEditor() 方法示例,也可以在Web Connection安装的演示应用程序中运行这个示例。
2. 异步和定时操作
随着Web应用的成熟和复杂度的增加,除了对Web请求的简单响应外,还会出现一些新的需求,主要分为异步操作和定时操作两类。
2.1 异步操作的必要性
当一些传入请求处理时间过长,无法及时返回结果时,就需要异步操作。例如,长时间运行的查询,可能是在大型数据库上进行文本搜索或多次表连接的查询。当处理时间超过浏览器或Web服务器的超时值时,就绝对需要异步操作。但实际上,由于用户在长时间请求运行时没有得到任何反馈,可能会采取一些不良行为,如重新提交请求或离开网站,因此任何可能需要超过几秒才能完成的请求都应考虑迁移到异步操作。
以下是长时间请求可能导致的问题:
| 问题类型 | 具体描述 |
| ---- | ---- |
| 浏览器超时 | 客户端浏览器超时,用户收到无法控制的通用消息,不知道服务器是否收到请求、网站是否关闭或自己是否出错。 |
| 服务器超时 | Web服务器或Web Connection DLL放弃并超时,向用户发送错误页面,指示没有生成输出。在基于文件的消息传递下,应用程序会完成处理,但响应无法送达;在COM消息传递下,应用程序可能在足够长的超时后终止。 |
| 重复请求 | 用户习惯了网站其他页面的快速响应,会认为自己操作有误而再次提交请求,导致第一个进程完成后被放弃,相同请求重复执行,响应时间更长,网站资源被加倍占用。 |
| 用户离开 | 用户认为网站无响应而放弃,可能会导航到竞争对手的网站。 |
| 不良印象 | 即使用户在超时时间内得到响应,但在等待期间没有任何反馈,可能会对网站质量产生不良印象。 |
2.2 异步请求的工作原理
异步请求的解决方案是将请求处理分为两部分。首先,快速返回一个消息,表明请求正在处理,同时通过某种消息传递机制安排一个单独的应用程序来执行实际处理。具体步骤如下:
graph LR
A[Web Connection方法接收请求并分析需求] --> B[将请求记录到队列]
B --> C{选择架构}
C -->|启动后端应用| D[使用RUN命令或ShellExecute API启动后端应用]
C -->|后台轮询| E[创建后台运行的后端应用,持续轮询队列]
B --> F[返回请求正在处理的响应,包含HTML刷新指令]
F --> G[浏览器根据刷新指令定期轮询应用程序]
D --> H[后端应用处理请求,更新队列状态]
E --> H
H --> I{处理完成?}
I -->|否| J[Web Connection应用返回状态更新页面]
I -->|是| K[Web Connection应用返回最终结果]
J --> G
- 典型的Web Connection方法接收传入请求并分析需求(表单变量、URL参数等)。
- Web Connection方法不处理完整请求,而是将请求记录到队列中,由后端应用程序处理。这个队列可以简单地是一个作为自由表创建的单个DBF文件。
- 根据选择的架构,Web Connection方法可以使用Visual FoxPro的
RUN命令或ShellExecute API启动后端应用程序。如果这种需求不频繁,可以选择这种方法;或者创建一个在后台运行并持续轮询队列中新条目的后端应用程序,这样可以提高效率,但需要确保后端应用程序始终运行并在需要时可用,例如将其包含在Windows启动组中。 - Web Connection方法立即返回一个响应,表明请求正在处理,并包含一个HTML刷新指令(使用META标签),使浏览器每隔几秒轮询Web Connection应用程序,查看处理是否完成。
- 后端应用程序在处理过程中更新队列中请求的状态,并在处理完成后插入最终响应。
- 在此期间,由于每个响应中包含的META刷新标签,Web Connection应用程序会收到客户端浏览器的重复轮询请求。每次请求时,方法会检查队列中项目的状态,要么返回状态更新页面(带有进一步的刷新指令),要么在处理完成后返回最终结果。
- 可以添加额外的代码来处理超时、用户取消等情况。
2.3 wwAsyncWebRequest类
从3.65版本开始,Web Connection包含一个新的 wwAsyncWebRequest 类,用于处理异步请求。这个类是一个实用类,由Web Connection应用程序和后端应用程序调用,通过消息表进行进度通信。Web Connection应用程序将新请求写入表中,并在收到新的轮询请求时检查表中的状态消息;后端应用程序检查表中要处理的新请求,并在处理请求时更新表中的请求状态。 wwAsyncWebRequest 类将这种通信抽象为一个易于部署的类。
2.4 异步处理示例
以下是一个简单的异步处理示例,基于TODO应用程序。TODO应用程序的 Class_Tsk 类有一个 ListTasks 方法,用于列出所有未完成的任务。假设未来未完成任务数量过多,这个请求无法及时完成,通过在处理过程中插入延迟来模拟这种情况。
以下是 SlowListTasks 方法的代码:
**************************************
FUNCTION SlowListTasks
**************************************
SET PROCEDURE TO wwAsyncWebRequest ADDITIVE && not set in WCONNECT.PRG
LOCAL loAsync, lcAction, lcJobId, lnRefreshPeriod, ;
lnRetries, lnTry, lcMessage
loAsync = CREATEOBJECT( "wwAsyncWebRequest")
* See if this is a new request or polling for a current request:
lcAction = Request.QueryString( 'Action')
lcAction = IIF( EMPTY( m.lcAction), 'New', PROPER( m.lcAction))
IF m.lcAction == 'New'
lcJobId = loAsync.SubmitEvent( , "Slow Task List")
* Optionally, fire up the process here. (Note: You
* can also run a continuous background process that
* polls for new requests.)
LOCAL lcRunCmd
lcRunCmd = [RUN /n4 TODOAsyncHandler.EXE ] + m.lcJobId
&lcRunCmd
ELSE && not new
lcJobId = Request.QueryString( 'JobId')
ENDIF
* Set refresk periond (in seconds):
lnRefreshPeriod = 1
* How many polls before we give up:
lnRetries = 50
* Which retry are we on:
lnTry = 0
DO CASE
CASE m.lcAction == 'Check'
LOCAL lnStatus
lnStatus = loAsync.CheckForCompletion( m.lcJobId)
* 0 means still running, negative numbers are error conditions
DO CASE
CASE m.lnStatus = -2 && bad ID
THIS.ErrorMsg( "Invalid Request", ;
"Sorry, your request was either invalid or has been lost." + ;
" Please try again." )
RETURN
CASE m.lnStatus = -1 && cancel
THIS.ErrorMsg( "Request Cancelled", ;
"Your request has been cancelled." )
RETURN
CASE m.lnStatus = 1 && completed
LOCAL lcResult, loReturnXML
lcResult = loAsync.oEvent.ReturnData
#IF .F. && return XML
Response.ContentTypeHeader( "text/xml")
Response.Write( m.lcResult )
#ELSE && return HTML page
loReturnXML = CREATEOBJECT( "wwXML")
loReturnXML.XMLToCursor( m.lcResult, "TaskList")
Response.ExpandScript( Process.cHTMLPAGEPATH + ;
"tasklist.wcs", 3, THIS.Header)
USE IN TaskList
#ENDIF
RETURN
ENDCASE
CASE m.lcAction == "Cancel"
loAsync.CancelEvent( m.lcJobId)
THIS.StandardPage( "Your request has been cancelled.")
RETURN
ENDCASE
* If we get this far, we want to return a status update page:
lnTry = loAsync.oEvent.ChkCounter
IF m.lnTry > m.lnRetries
THIS.StandardPage( "Request Timeout", ;
"Sorry, but your request could not be completed in a " + ;
"timely fashion." + ;
" Please try again later.")
RETURN
ENDIF
lcMessage = 'Please wait. Your request is being processed. <br>' + ;
[<table cellpadding=0 cellspacing=0 ] + ;
[style="border: medium solid orange;" width=] + ;
TRANSFORM( 5 * m.lnRetries) + [>] + ;
[<tr><td style="background-color: blue;"] + ;
[ width=] + TRANSFORM( 5 * m.lnTry) + ;
[> </td><td> </td></tr></table>] + ;
[<BR>] + loAsync.oEvent.Status
THIS.StandardPage( "Asynchronous Task List", m.lcMessage, , ;
m.lnRefreshPeriod, ;
'SlowListTasks.tsk?action=Check&JobId=' + m.lcJobId )
ENDFUNC && SlowListTasks
后端应用程序的代码如下:
* ToDoAsyncMain.PRG
#INCLUDE WCONNECT.H
PARAMETERS lcJobId
IF EMPTY( m.lcJobId)
RETURN
ENDIF
SET EXCLUSIVE OFF
SET TALK OFF
SET SAFETY OFF
DO wconnect
SET PROCEDURE TO wwasyncwebrequest ADDITIVE
SET PROCEDURE TO ToDoConfig ADDITIVE
SET PROCEDURE TO queryengine ADDITIVE
ON ERROR DO AsyncError IN ToDoAsyncMain WITH ;
ERROR(), MESSAGE(), MESSAGE(1), SYS(16), LINENO()
DO PATH WITH '.\Data'
* Need some dummy objects to "simulate" the Server
* and Process objects in the TODO environment:
PRIVATE goWCServer, Process
goWCServer = CREATEOBJECT( "DummyServer") && see below
Process = CREATEOBJECT( "DummyProcess") && see below
LOCAL loAsync, lcTitle
loAsync = CREATEOBJECT( "wwAsyncWebRequest")
IF NOT loAsync.LoadEvent( m.lcJobId)
ON ERROR
RETURN
ENDIF
loAsync.oEvent.Started = DATETIME()
loAsync.SaveEvent()
lcTitle = TRIM( loAsync.oEvent.Title)
DO CASE
CASE lcTitle == "Slow Task List"
= DelayEvent( loAsync, m.lcJobId, 20)
LOCAL loQuery, loXML, lcResult
loQuery = CREATEOBJECT("QueryEngine")
loQuery.Execute( "TASKLISTALL")
loXML = CREATEOBJECT( "wwXML")
loXML.nCreateDataStructure = 2 && include DTD
lcResult = loXML.CursorToXML()
loAsync.CompleteEvent( m.lcJobId, m.lcResult)
ENDCASE
ON ERROR
RETURN
* ----------------------------------------------------- *
FUNCTION AsyncError( lnErr, lcMess, lcCode, lcMethod, lnLine)
ON ERROR
WAIT WINDOW TIMEOUT 2 m.lcMess + [ (error ] + TRANS( m.lnErr) + [)]
IF NOT FILE( FULLPATH( ".\AsyncError.DBF"))
SELECT 0
CREATE TABLE AsyncError FREE ;
( Error I, Message C(80), Code M, Method C(50), Line I, MoreInfo M)
USE
ENDIF
LOCAL lcLogString, lnLevel, lnPtr
lnLevel = PROGRAM(-1)
lcLogString = ""
lcLogString = m.lcLogString + "Program Stack" + CRLF + CRLF
FOR lnPtr = lnLevel TO 1 STEP -1
lcLogString = m.lcLogString + ;
TRANS( m.lnPtr) + [. ] + PROGRAM( m.lnPtr) + CRLF
ENDFOR
INSERT INTO AsyncError ;
( Error, Message, Code, Method, Line, MoreInfo) ;
VALUES ;
( lnErr, lcMess, lcCode, lcMethod, lnLine, lcLogString )
ENDFUNC && AsyncError
* ----------------------------------------------------- *
FUNCTION DelayEvent( loAsync, lcId, lnSecs)
DECLARE Sleep IN WIN32API INTEGER
LOCAL ii
FOR ii = 1 TO m.lnSecs
Sleep( 1000)
IF NOT loAsync.LoadEvent( m.lcID)
LOOP
ENDIF
IF loAsync.oEvent.Cancelled
RETURN
ENDIF
loAsync.oEvent.Status = TRANS( m.lnSecs - m.ii) + " seconds remaining."
loAsync.SaveEvent()
ENDFOR
ENDFUNC && DelayEvent
* ----------------------------------------------------- *
DEFINE CLASS DummyServer AS Custom
oConfig = NULL
cIniFile = "ToDo.INI"
FUNCTION INIT
THIS.SetEnvironment()
ENDFUNC
* ---------------------------------------------------- *
FUNCTION SetEnvironment
THIS.cIniFile = ADDBS( GetAppStartPath()) + THIS.cIniFile
LOCAL lcStr
THIS.oConfig = CREATE( "ToDoServerConfig")
THIS.oConfig.cFileName = THIS.cIniFile
THIS.oConfig.Load()
ENDFUNC
ENDDEFINE
* ----------------------------------------------------- *
DEFINE CLASS DummyProcess AS Custom
cTempPath = ""
cShowPlanResults = ""
* ---------------------------------------------------- *
FUNCTION INIT
THIS.cTempPath = goWcServer.oConfig.oTodo.cTempPath
ENDFUNC
ENDDEFINE
在测试新的异步操作之前,需要构建后端可执行文件。创建一个名为 ToDoAsyncHandler 的新项目,并将上述程序作为主程序添加到项目中。该项目可以与主TODO应用程序放在同一文件夹中,然后将项目构建为可执行文件 ToDoAsyncHandler.EXE 。
完成上述步骤后,就可以测试系统了。启动TODO应用程序,然后在浏览器中输入 http://localhost/todo/SlowListTasks.tsk 。如果一切正常,在请求运行的延迟期间,应该会看到如预期的响应。
此外, wwAsyncWebRequest 类还支持在请求完成前取消长时间运行的请求。可以通过在 SlowListTasks 方法中构建消息的代码中添加以下内容来轻松实现:
lcMessage = m.lcMessage + ;
[<p align=center>] + ;
Response.Href( 'SlowListTasks.tsk?action=Cancel&JobId=' + m.lcJobId, ;
"Cancel", .T.)
这样,在处理运行时,状态结果页面将显示取消链接,用户可以点击该链接取消请求。
通过以上的方法和示例,可以有效地进行Web应用的高级故障排除、维护以及处理异步操作,提高系统的性能和用户体验。
3. 定时操作的需求与实现思路
定时操作与异步操作不同,它并非对传入请求的响应,而是代表着需要定期执行的重复性任务。这些任务对于 Web 应用的正常运行和数据管理至关重要,以下是一些常见的定时操作示例:
| 操作类型 | 具体描述 |
| ---- | ---- |
| 发送排队消息 | 将排队的消息发送到邮件服务器,确保消息及时传递。 |
| 数据库实时备份 | 制作数据库的实时备份副本,保障数据的安全性和可恢复性。 |
| 发送错误日志 | 将错误日志通过电子邮件发送给管理员,方便及时发现和解决问题。 |
| 删除临时文件 | 清理系统中的临时文件,释放磁盘空间。 |
由于 Web 应用通常只响应传入的 Web 请求,无法自行执行这些定时任务。如果创建一个包含请求这些操作的 URL 页面,还需要员工及时触发每个操作,这既成本高昂又容易出错。因此,需要一种更自动化的解决方案来实现定时操作。
4. 异步与定时操作的结合与优化
在实际的 Web 应用中,异步操作和定时操作往往需要结合使用,以满足复杂的业务需求。以下是一些结合和优化的思路:
4.1 任务调度与异步处理
可以使用定时任务来触发异步操作。例如,定时任务可以定期检查是否有需要处理的长时间请求,并将这些请求放入异步处理队列中。这样可以确保系统资源的合理利用,避免长时间请求对正常业务造成影响。
4.2 错误处理与重试机制
在异步和定时操作中,错误处理和重试机制非常重要。当异步操作或定时任务失败时,系统应该能够捕获错误并进行相应的处理。可以设置重试次数和重试间隔,确保任务最终能够成功执行。
4.3 性能监控与优化
对异步和定时操作进行性能监控是优化系统的关键。可以记录每个操作的执行时间、成功率等指标,分析系统的性能瓶颈,并采取相应的优化措施。例如,如果发现某个异步操作的执行时间过长,可以考虑优化算法或增加系统资源。
5. 总结
Web 应用的高级故障排除、维护以及异步和定时操作是确保系统稳定运行和提供良好用户体验的重要环节。通过创建基于 Web 的维护接口、合理安排数据备份、利用 COM 进行在线维护等方法,可以有效地进行系统的维护和管理。对于长时间请求,采用异步操作可以避免用户的不良行为,提高系统的响应速度和资源利用率。同时,定时操作可以满足系统的周期性需求,保障数据的安全性和业务的正常运行。
在实际应用中,需要根据具体的业务需求和系统架构,选择合适的方法和工具来实现这些操作。通过不断地优化和改进,可以提高 Web 应用的性能和可靠性,为用户提供更好的服务。
以下是一个简单的定时任务示例,使用 Python 的 schedule 库来实现每天凌晨 2 点执行数据库备份的定时任务:
import schedule
import time
import subprocess
def backup_database():
try:
# 执行数据库备份命令
subprocess.run(["python", "backup_script.py"])
print("Database backup completed successfully.")
except Exception as e:
print(f"Database backup failed: {e}")
# 每天凌晨 2 点执行备份任务
schedule.every().day.at("02:00").do(backup_database)
while True:
schedule.run_pending()
time.sleep(1)
这个示例展示了如何使用 schedule 库来实现定时任务。在实际应用中,可以根据需要修改备份命令和执行时间。
通过以上的方法和示例,希望能够帮助开发者更好地处理 Web 应用的高级故障排除、维护以及异步和定时操作,提升系统的整体性能和稳定性。
超级会员免费看
1279

被折叠的 条评论
为什么被折叠?



